循环遍历 List调用 remove 方法时遇到ConcurrentModificationException 异常


前言

循环遍历 List,调用 remove 方法删除元素,往往会遇到ConcurrentModificationException异常,原因是什么,正确的打开方式又是什么呢?


一、导致异常的例子

为了便于大家理解,下面举一个导致异常的例子

以下是一个使用 for-each 循环遍历 ArrayList 时调用 remove() 方法导致抛出 ConcurrentModificationException 的例子:

import java.util.ArrayList;
import java.util.List;

public class Example {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        // 使用 for-each 循环遍历并尝试移除元素
        for (Integer num : list) {
            if (num == 3) {
                list.remove(num);  // 直接修改列表结构
            }
        }
    }
}

输出结果:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997)
    at Example.main(Example.java:13)

二、所以导致异常的原因是什么呢?

在上面的代码中,使用了 for-each 循环来遍历 ArrayListfor-each 循环底层是使用 Iterator 实现的,但是在 for-each 循环中直接调用 list.remove() 修改了列表的结构,ArrayListmodCount 值发生了变化,但 Iterator 中记录的初始 modCount没有同步更新,这就导致抛出了ConcurrentModificationException

注:虽然代码上看是 for (Integer num : list),但是底层实现实际上是通过隐式创建一个 IteratorArrayList 进行迭代的。等价代码如下:

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    Integer num = iterator.next();
    // Your logic
}

如果上面的原因还没看明白,我们继续往下看

三、list.remove(num)它是怎么删除的,为什么会使列表的结构修改?

大家可能还存在疑惑的地方:list.remove(num)它是怎么删除的,为什么会使列表的结构修改?

首先来看看ArrayListremove(Object o) 方法的实现:

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++) {
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
        }
    } else {
        for (int index = 0; index < size; index++) {
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
        }
    }
    return false;
}
  • ArrayList 使用一个数组 elementData 来存储所有元素。size 是当前列表中实际元素的数量。
  • 该方法会遍历数组 elementData,寻找与传入的对象 o 相等 的第一个元素。
    • 如果 onull,它会找出第一个 null 元素。
    • 如果 o 不为 null,它使用 o.equals() 来判断对象是否相等。

当找到了相等的元素后,它会调用 fastRemove(index) 方法来执行删除操作。

然后我们再来看看fastRemove(index)方法的底层实现:

private void fastRemove(int index) {
    modCount++;  // 1. 更新修改计数 modCount
    int numMoved = size - index - 1;
    if (numMoved > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    }
    elementData[--size] = null; // 2. 将最后一个元素置为 null,帮助 GC
}
  1. 更新 modCount

modCount++ArrayList 记录结构修改次数的变量。它会在任何结构性修改(如 add()remove())时增加,用于迭代器的并发修改检查。如果你在遍历过程中修改了 ArrayList 的结构(比如删除元素),****modCount 会与迭代器的初始状态不一致,导致抛出 ConcurrentModificationException****。

  1. 执行删除操作

删除元素的核心是通过 System.arraycopy(),它的作用是把数组中从 index + 1 开始的部分,往前移动一位,覆盖掉删除的元素。具体步骤如下:

  • elementData 是一个包含列表元素的数组。
  • index 是要删除的元素的下标。
  • numMoved = size - index - 1 表示从 index + 1 开始到 size - 1 的元素总数。
  • System.arraycopy(elementData, index + 1, elementData, index, numMoved)index + 1 及其之后的元素向前移动一位,覆盖掉 index 位置的元素。
  1. 释放最后一个元素

数组中的最后一个元素会被设置为 null,这样可以帮助垃圾回收器(GC)回收不再使用的对象。size-- 减少了数组的大小,表示列表长度减少了。

1.所以总结下来整个删除流程

  1. 当调用 list.remove(num) 时,ArrayList 会遍历其内部的数组 elementData,寻找与 num 相等 的元素。
  2. 找到后,调用 fastRemove(index),该方法通过 System.arraycopy()index + 1 后面的元素向前移动一位,覆盖被删除的元素。
  3. modCount 自增,通知迭代器有结构修改。
  4. 将数组的最后一个元素设为 null,并减少 size,从而完成删除操作。

2.所以list.remove(num)会使列表的结构修改的原因就在于:

在删除的时候, ArrayListmodCount 值发生了变化,但 Iterator 中记录的初始 modCount 值没有同步更新,这就导致抛出了 ConcurrentModificationException

四、正确方式是什么呢

可以使用 Iteratorremove() 方法来删除元素,Iterator 会同步更新 modCount

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Example {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        // 使用 Iterator 安全地遍历和删除
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer num = iterator.next();
            if (num == 3) {
                iterator.remove();  // 安全删除元素
            }
        }

        System.out.println(list);  // 输出: [1, 2, 4, 5]
    }
}

输出结果:

[1, 2, 4, 5]

使用 Iteratorremove() 方法,同步更新 modCount,就可以安全地修改列表结构了,同时避免 ConcurrentModificationException

如果有些宝子对迭代器的remove是怎么删除元素的感兴趣,可以接着往下看

五、Iterator.remove() 是怎么删除元素的呢?

Iterator.remove() 方法在 ArrayList 中的实现通过其内部类 Itr 来完成,该类实现了 Iterator 接口

当我们在迭代过程中调用 iterator.remove(),它实际上会调用 Itr 类的 remove() 方法,该方法通过操作 ArrayListremove() 来实现,并确保修改集合的同时保持迭代器的一致性。

private class Itr implements Iterator<E> {
    int cursor;       // 当前迭代器的位置,指向下一个元素的索引
    int lastRet = -1; // 上一次返回的元素的索引,-1 表示还没有返回过
    int expectedModCount = modCount; // 预期的 modCount,用于检查并发修改

    public boolean hasNext() {
        return cursor != size;
    }

    @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]; // 返回当前元素,并更新迭代器状态
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException(); // 检查是否能合法删除
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet); // 调用 ArrayList 的 remove 方法
            cursor = lastRet; // 将 cursor 回退到上一个元素的位置
            lastRet = -1; // 重置 lastRet
            expectedModCount = modCount; // 更新预期的 modCount
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException(); // 异常处理
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount) // 检查 modCount 是否改变
            throw new ConcurrentModificationException(); // 抛出并发修改异常
    }
}

ArrayList 中,创建迭代器时,会将 modCount 的值保存为 expectedModCount。每次你通过迭代器调用 next()remove() 时,迭代器会检查当前的 modCountexpectedModCount 是否相等。如果不相等,说明集合在迭代过程中被修改了(而不是通过迭代器进行的安全修改),此时会抛出 ConcurrentModificationException

六、modCount 的设计是为了什么呢?

其实modCount 的设计是为了在 单线程迭代 中,防止你在遍历集合时 无意或错误地修改集合的结构,从而引发逻辑错,所以通过异常提示,提醒开发者应遵循正确的迭代操作方式。并不是为了处理多线程并发问题,在多线程环境仍然是有并发问题的,它并非线程安全的。

如果还是不理解,那么我们来看看假如没有 modCount 检测,而我们直接对集合进行修改,会发生什么?

还是同样举个例子

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

Iterator<Integer> iterator = list.iterator();

while (iterator.hasNext()) {
    Integer value = iterator.next();
    if (value == 3) {
        list.remove(value);  // 不通过迭代器删除
    }
}

可能出现的问题:

  1. 元素顺序问题:当你删除 3 后,后面的元素 45 会向前移动。此时迭代器的 cursor 仍然指向下一个未删除的元素。然而,由于集合内部的元素已经移动,迭代器可能会跳过元素或重复访问同一个元素。
    1. 比如:如果 cursor 还没更新,那么在删除 3 后,迭代器会跳过 4,导致无法正确遍历所有元素。
  2. 访问无效元素:如果迭代器继续遍历,但集合已经发生了修改,它可能会尝试访问已经被删除的元素,导致程序崩溃或抛出 IndexOutOfBoundsException

看到这儿相信各位宝子应该就明白了,为什么不能list.remove修改列表的结构,而是采用迭代器的remove了吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值