Java循环陷阱:在for-each中删除集合数据导致的异常原理

437 阅读3分钟

当我们在遍历 Java 集合(例如 ArrayList)时,如果在遍历过程中尝试修改集合(例如添加或删除元素),就会遇到 ConcurrentModificationException 异常。大家可能会对这个异常感到困惑,因此在这篇文章中,我将详细解释这个异常的产生原理,以及如何正确地在遍历集合的同时修改它。

ConcurrentModificationException 的产生原理

在 Java 集合类中,有一个 modCount 字段,这个字段记录了集合的“结构修改次数”。每当我们添加或删除集合元素时,modCount 就会增加。

image.png 这是集合中add的源码

  * @param e element to be appended to this list
         * @return <tt>true</tt> (as specified by {@link Collection#add})
         */
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }

可以看到当add方法调用时modCount++ image.png 当我们使用 for-each 循环或者 Iterator 遍历集合时,会创建一个迭代器对象,这个迭代器对象有一个 expectedModCount 字段,其值在创建时被赋值成 modCount来保持一致。

image.png

在遍历的时候,迭代器会在每次开始时检查 expectedModCount 是否等于 modCount。如果这两个值不等,迭代器就会抛出 ConcurrentModificationException 异常。

下面是一个简单的代码示例,演示了在 for-each 循环中尝试修改 ArrayList 时会抛出 ConcurrentModificationException 异常:

    public static void main(String[] args) {
            List<String> arrayList = new ArrayList<String>();
            arrayList.add("1");
            arrayList.add("2");//modCount 为 2
            Iterator<String> iterator = arrayList.iterator();
            while(iterator.hasNext()) {
                String list = iterator.next();
                if("2".equals(list)){
                    arrayList.remove(list);//因为调用方是arrayList所以modCount会加1,但是**expectedModCount 还是2
                }
            }
            System.out.println("arrayList:"+arrayList);
        }

在这个代码中,在arrayList中调用了2次add方法所以modCount等于2。 循环开始时创建了一个迭代器,这时 expectedModCount 被设置为 modCount 的当前值,也就是 2。

然后在遍历过程中,我们尝试删除 "2",但是调用方是arrayList这导致 modCount 增加到 3,但 expectedModCount 的值仍然是 2,因此在下一次迭代开始时,迭代器抛出了 ConcurrentModificationException 异常。

大家可能会好奇,不是只有俩个值吗!为甚么会在循环一次?

这与 Java 的 ArrayList 的迭代器实现有关。为了解释这个问题,我们需要深入了解 ArrayListIterator 是如何工作的。

ArrayListIterator 在每次调用 next() 方法时,会检查是否还有更多元素。这是通过比较一个 cursor 的值(该值表示迭代器当前所在的位置)和 ArrayList 的大小来实现的。如果 cursor 不等于 ArrayList 的大小,那么 hasNext() 方法就返回 true

image.png

第一次循环进来 cursor=0,size=2,进行下一次循环,

cursor=1,size=2 ,进行下一次循环,注意:在刚才的代码中,在遍历到第二个元素时删除了一个数据

所以第三次判断 cursor=2,size=1 仍然为true 继续向下执行

迭代器会检查 modCount 是否等于 expectedModCount。此时 modCount 已经增加到3,而 expectedModCount 仍然是2,因此迭代器会抛出 ConcurrentModificationException 异常。

正确的修改方法

那么,我们应该如何在遍历集合的过程中正确地修改它呢?其实,Iterator 为我们提供了 remove 方法,我们可以通过这个方法来删除当前元素。这个方法在删除元素后,会正确地更新 expectedModCount 的值,使其等于 modCount,从而防止 ConcurrentModificationException 异常的产生。

以下是一个使用 Iteratorremove 方法在遍历过程中删除元素的代码示例:

javaCopy code
List<String> arrayList = new ArrayList<String>();
arrayList.add("1");
arrayList.add("2");
Iterator<String> iterator = arrayList.iterator();
while(iterator.hasNext()) {
    String list = iterator.next();
    if("2".equals(list)){
        iterator.remove();
    }
}

这段代码中,我们使用 iterator.remove(); 来删除元素,而不是 arrayList.remove(list); 。这样,迭代器就能正确地跟踪 modCount 的变化,从而避免 ConcurrentModificationException 异常。

总结起来,如果我们想在遍历集合的过程中修改集合,应该使用 Iteratorremove 方法。如果我们需要在遍历过程中添加元素,应该选择其他方法,例如先收集需要添加的元素,然后在遍历结束后添加到集合中。 实际开发中可以用removeIf方法来做删除。