foreach循环(增强for循环)的坑 - ConcurrentModificationException

2,177 阅读3分钟

        最近在看《阿里巴巴Java开发手册》时,(五)集合处理-7【强制】不要在foreach循环里进行元素的remove/add操作。这一条规则中,有一段非常有意思的代码

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}

执行这段代码,没有任何问题,foreach循环不会抛出任何异常,而且元素移除成功。

但是,当我们把if ("1".equals(item))改成if ("2".equals(item))时,程序会抛出ConcurrentModificationException。如下图所示:


通过分析foreach实现机制以及抛出的代码行数,很容易得知其原因。

foreach实现机制

        我们知道,foreachJava的一个语法糖,通过反编译测试类生成的代码,我们知道它实际上是由Iterator实现的。反编译的结果如下:

List<String> list = new ArrayList();
list.add("1");
list.add("2");
Iterator var2 = list.iterator();

while (var2.hasNext()) {
    String item = (String)var2.next();
    if ("2".equals(item)) {
        list.remove(item);
    }
}

        结合程序抛出的异常栈信息,我们可以得知,异常是在String item = (String)var2.next();这一行抛出的(只有这一行调用了next()操作)。

异常原因

        找到报错的代码行,我们再来分析报错的原因。既然在String item = (String)var2.next();报错,说明while循环的条件是通过了。也就是说,我们在移除了最后一个元素后,迭代器竟然并未感知到,hasNext()依然返回true。我们来看看ArrayList.IteratorhasNext()是如何实现的:

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;   // 修改次数,modCount来自于外部的ArrayList类;
                                       // 而expectedModCount则由Itr记录
    ...
    public boolean hasNext() {
        return cursor != size;   // size来自于外部的ArrayList类
    }
    ...
}

    ArrayList.iterator().hasNext()的实现很简单,就是判断当前下标是否与数组大小相同。这就说明,当删除ArrayList最后一个元素时,cursor != size。为什么会这样呢?我们再来看一下next()的实现:

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;   // 修改次数,modCount来自于外部的ArrayList类;
                                       // 而expectedModCount则由Itr记录
    ...
    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size) 
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
    ...
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

        我们看cursor值 的变化,实际上,cursor的值在每次进行元素值对比前,就已经跳到下一个位置。当我们删除第1个元素后,cursor的值是1,与当前ArrayList.size(1)是相等的,hasNext()返回falsewhile循环结束;而当我们删除第2个元素时,cursor的值是2,与当前ArrayList.size(1)不相等,因此hasNext()返回true

        我们再看next()为什么抛出异常。ArrayList.Itrnext()操作中,首先检查modCount是否等于expectedModCount,如果不相等,则抛出ConcurrentModificationException,程序中的异常也正是在此处抛出的。expectedModCountArrayList.Itr维护,而我们程序中用ArrayList.remove()移除元素,修改的是ArrayList.modCount,这两个值显然不相等(expectedModCount == 0modCount == 1)。正是由于hasNext()错误地返回了true,导致我们调用next()而抛出了异常。

结论

        从上文的分析中我们可以得出结论,由于foreach语法糖的特性,它并不是严格意义上的java语法,不能像Java标准语法那样灵活地使用。从其生成的代码来看,foreach循环不能进行元素的修改操作,而只能进行元素的遍历操作。