Java中使用for-each遍历踩坑问题

Java 提供了一个 Iterable 接口返回一个迭代器,常用的 Collection、List、Set 等都实现了这个接口,该接口的 iterator() 方法返回一个标准的 Iterator 实现,实现 Iterable 接口允许对象成为 for-each 语句的目标来遍历底层集合序列,因此使用 for-each 方式遍历列表在编译后实质是迭代器的形式实现。之所以会出现 ConcurrentModificationException 异常我们需要去看下最常见的 ArrayList 中 iterator() 方法的实现(别的集合 iterator 类似),如下:

public class Itr implements Iterator {

    protected int limit = ArrayList.this.size; //集合列表的个数尺寸
    int cursor; //下一个元素的索引位置
    int lastRet = -1; //上一个元素的索引位置
    int expectedModCount = modCount;

    @Override
    public boolean hasNext() {
        return cursor < limit;
    }

    @Override
    public E next() {
        // modCount用于记录ArrayList集合的修改次数,初始化为0,
        // 每当集合被修改一次(结构上面的修改,内部update不算),
        // 如add、remove等方法,modCount + 1,所以如果modCount不变,
        // 则表示集合内容没有被修改。
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        int i = cursor;
        //如果下一个元素的索引位置超过了集合长度抛出异常
        if (i >= limit) {
            throw new NoSuchElementException();
        }
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        // 调用一次cursor加一次
        cursor = i + 1;
        //返回当前一个元素
        return (E) elementData[lastRet = i];
    }

    @Override
    public void remove() {
        // lastRet每次在remove成功后都需要在next()中重新赋值,
        // 否则调用一次后再调用为-1异常,因此使用迭代器的remove方法
        // 前必须先调用next()方法。
        if (lastRet < 0) {
            throw new IllegalStateException();
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
            limit--;
        } catch (IndexOutOfBoundsException e) {
            throw new ConcurrentModificationException();
        }
    }
    ......
}

通过上面的源码发现迭代操作中都有判断 modCount != expectedModCount的操作,在 ArrayList 中 modCount 是当前集合的版本号,每次修改(增、删)集合都会加 1,expectedModCount 是当前迭代器的版本号,在迭代器实例化时初始化为 modCount,所以当调用 ArrayList.add() 或 ArrayList.remove() 时只是更新了 modCount 的状态,而迭代器中的 expectedModCount 未修改,因此才会导致再次调用 Iterator.next() 方法时抛出 ConcurrentModificationException 异常。而使用 Iterator.remove() 方法没有问题是因为 Iterator 的 remove() 方法中有同步 expectedModCount 值,所以当下次再调用 next() 时检查不会抛出异常。这其实是一种快速失败机制,机制的规则就是当多个线程对 Collection 进行操作时若其中某一个线程通过 Iterator 遍历集合时该集合的内容被其他线程所改变,则抛出 ConcurrentModificationException 异常。
因此在使用 Iterator 遍历操作集合时应该保证在遍历集合的过程中不会对集合产生结构上的修改,如果在遍历过程中需要修改集合元素则一定要使用迭代器提供的修改方法而不是集合自身的修改方法。此外, for-each 循环遍历编译后实质会替换为迭代器实现,使用迭代器的 remove() 方法前必须先调用迭代器的 next() 方法且不允许调用一次 next() 方法后调用多次 remove() 方法。

你可能感兴趣的:(Java中使用for-each遍历踩坑问题)