源码理解为什么不要在 foreach 循环里进行元素的 remove或add 操作

源码解析“不要在 foreach 循环里进行元素的 remove或add 操作”

前言

《阿里巴巴Java开发手册》里看到里面提到这样一条编程规约让我不理解,即:

【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

正例:

Iterator<String> it = a.iterator();
while (it.hasNext()) {
    String temp = it.next();
    if (删除元素的条件) {
        it.remove();
    }
}

反例:

List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
    if ("1".equals(temp)) {
        a.remove(temp);
    }
}

说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的 结果吗?

跟着提供的正例反例,我进行测试,得到运行结果如下:

  • 正例中remove操作都不会报错;
  • 反例中remove"1"不会报错,当if语句中"1"换成"2"之后,则报java.util.ConcurrentModificationException异常

运行结果还真是挺有意思,我不理解为什么会出现这样的现象,于是便找下真正的原因。

例子展示

为了方便理解,我还是以ArrayList为例,代码重新组织了下,写了个Demo类(JDK1.8),代码如下:

public class ListMod {

    public static void main(String[] args) {
        String removeEle = "1";
        iteratorRemove(removeEle);
        foreachRemove(removeEle);
    }

    /**
     * 用迭代器操作移除
     * */
    public static void iteratorRemove(String removeEle){
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            if (removeEle.equals(item)) {
                iterator.remove();
            }
        }
    }

    /**
     * foreach里进行移除
     * */
    public static void foreachRemove(String removeEle){
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for (String item : list) {
            if (removeEle.equals(item)) {
                list.remove(item);
            }
        }
    }
}

测试将removeEle分别取"1"、“2”、“3”、“4”,得到测试结果如下:

参数\结果用迭代器操作移除foreach里进行移除
“1”正常ConcurrentModificationException异常
“2”正常ConcurrentModificationException异常
“3”正常正常
“4”正常ConcurrentModificationException异常

是不是非常奇怪,上述代码为什么偏偏removeEle=3的时候,不会报异常?

源码解析

首先,可以根据代码编辑器控制台错误栈信息来看下报错的位置,是在ArrayList的私有内部类Itr的checkForComodification方法中报出的错误

private class Itr implements Iterator<E> {
    
    /****** 省略其他代码 ******/
    
    /**
     * 检查并发修改 Concurrency Modification
     * 主要是用来实现fail-fast机制,阻止多线程的并发操作
     */
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

错误的原因是由于属性modCount != expectedModCount导致的,那这里有个问题,明明是foreach循环,为什么会使用到Itr里的checkForComodification方法呢?

foreach是个语法糖,我们无法直接点进某个方法查看遍历方法的实现。但是我们进行反编译看看。我用例子ListMod.class用JAD反编译得到结果文件ListMod.jad。打开这个jad文件,可以看到foreachRemove反编译之后的结果代码为:

public static void foreachRemove(String removeEle)
{
    List list = new ArrayList();
    list.add("1");
    list.add("2");
    list.add("3");
    list.add("4");
    Iterator iterator = list.iterator();
    do
    {
        if(!iterator.hasNext())
            break;
        String item = (String)iterator.next();
        if(removeEle.equals(item))
            list.remove(item);
    } while(true);
}

上述代码可以看出,foreach循环本质上也是使用iterator迭代器,再看iterator()方法,其实也是返回上面提到的ArrayList内部类Itr对象

public Iterator<E> iterator() {
    return new Itr();
}

回到上面报错的原因来,既然已经知道foreach的本质(可以看到无论是例子中的iteratorRemove方法或是foreachRemove方法,都其实用到了Itr中的hasNext和next方法),那就可以在Itr中hasNext和next打上断点,进行调试。

通过调试,最终可以确定foreachRemove里导致的报错,都是在Itr的next方法里调用的checkForComodification()方法被检查出异常,而当removeEle="3"时,移除"3"这个元素后(List倒数第二个元素),结合上面foreachRemove反编译后的代码,可以看出下一次循环中执行hasNext()方法返回false(cursor=size,至于为什么,继续看下面的remove(Object o)方法),因此它跳出了do-while的循环,便也不会执行后面的代码。

OK,既然根据调试已经找到了例子结果产生的原因,但是为了加深理解,还是要理解下这些相关的方法和变量的含义。先来看上面提到比较重要的Itr中的hasNext()和next()以及其相关的属性cursor和lastRet(其他代码暂时先省略)

private class Itr implements Iterator<E> {
    // index of next element to return 之后调用next返回的元素索引
    int cursor;
    // index of last element returned; -1 if no such 最近一次返回的元素索引。没有遍历前,值为-1
    int lastRet = -1; 
   	
	/****** 省略其他代码 ******/
    
    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        // 取得到next对应的索引
        int i = cursor;
        // 没有>=数组大小的元素索引
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        // 判断i是否大于实际的数组长度
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        // 光标+1
        cursor = i + 1;
        // 返回元素
        return (E) elementData[lastRet = i];
    }

    /****** 省略其他代码 ******/
}

从上述代码中可以看出,next()就是通过光标cursor不断向下一个移动进行元素获取,而lastRet则记录其最后一个移动获取到的元素索引。

再来看看和Remove操作相关的代码:

private class Itr implements Iterator<E> {
    // index of next element to return
    int cursor;
    // index of last element returned; -1 if no such
    int lastRet = -1; 
    // 迭代器认为支持 List 应该具有的 modCount 值。如果违反期望,如果违反了此预期,迭代器将检测到并发修改
    int expectedModCount = modCount;

    /****** 省略其他代码 ******/
    
    public void remove() {
        // lastRet < 0 不允许使用remove
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            // 移除最后遍历到lastRet对应元素,remove方法里会更改modCount
            ArrayList.this.remove(lastRet);
            // 光标 = lastRet
            cursor = lastRet;
            // lastRet置为-1,可阻止光标未再次移动时,重复进行remove
            lastRet = -1;
            // 期望修改次数等于修改次数
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    
    /****** 省略其他代码 ******/
}

/****** 省略不必要的代码 ******/

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 * 删除此list中指定位置的元素。将任何后续元素向左移动(从其索引中减去一个)。
 * @param index the index of the element to be removed 参数:要被移除元素的索引
 * @return the element that was removed from the list 返回:从list中移除的元素
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    rangeCheck(index);
	// 修改次数+1
    modCount++;
    // 获取该index对应的旧值
    E oldValue = elementData(index);
	// 拷贝时需要移动的个数 = 数组长度 - index -1
    int numMoved = size - index - 1;
    // 若移动的个数>0,则将index后的所有元素相当于都移到前面一个位置上
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 由于上面做了移动,则最后一个元素置为null(也兼容移除最后一个元素不进上面numMoved > 0判断的操作)
    elementData[--size] = null; // clear to let GC do its work
	// 返回移除的元素
    return oldValue;
}
// 省略不必要的代码
/**
 * Removes the first occurrence of the specified element from this list,
 * if it is present.  If the list does not contain the element, it is
 * unchanged.  More formally, removes the element with the lowest index
 * <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
 * (if such an element exists).  Returns <tt>true</tt> if this list
 * contained the specified element (or equivalently, if this list
 * changed as a result of the call).
 * 从该列表中删除第一个出现的指定元素(如果存在)。如果列表中不包含该元素,它将保持不变。更正式地说,删除元素值对应index最小的元素
 *
 * @param o element to be removed from this list, if present 要从此列表中删除的元素(如果存在)
 * @return <tt>true</tt> if this list contained the specified element 如果此列表包含指定的元素,返回true
 */
public boolean remove(Object o) {
    if (o == null) {
        // 若要删除的元素值为null,则遍历elementData数组,找第一个等于null的元素进行移除
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // 遍历elementData数组,找第一个与元素值相等的index进行删除
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

/****** 省略不必要的代码 ******/

/**
 * Checks if the given index is in range.  If not, throws an appropriate
 * runtime exception.  This method does *not* check if the index is
 * negative: It is always used immediately prior to an array access,
 * which throws an ArrayIndexOutOfBoundsException if index is negative.
 * 检查给定index是否在范围内。如果不是,则抛出响应的运行时异常。
 * 此方法不会检查索引是否为负:它总是在紧接在数据访问之前使用,如果索引为负,则会引发ArrayIndexOutOfBoundsException。
 */
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/****** 省略不必要的代码 ******/

/*
 * Private remove method that skips bounds checking and does not return the value removed.
 * 私有的remove方法,该方法跳过边界检查,不返回移除的值。
 */
private void fastRemove(int index) {
    // 该方法逻辑与remove(int index)基本一样
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

根据上述代码,可以看到例子中使用的方法:

  • iteratorRemove(Itr中remove() → remove(int index) ),会使modCount+1
  • foreachRemove(remove(Object o) → fastRemove(int index)),也会使modCount+1

而这两者的区别,则是Itr中remove()中有"expectedModCount = modCount"的代码设置。因此采用迭代器Iterator移除的方式可以通过checkForComodification()的检查。

再说下上面提到的例子中foreachRemove(“3”)不会报错的具体原因:因为在判断进倒数第二个元素进行移除时,执行fastRemove(int index)中"elementData[–size]=null"代码会让size = size-1,那对应最后一个元素的cursor就刚好等于此时的size了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值