文章目录
前言
循环遍历 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
循环来遍历 ArrayList
。for-each
循环底层是使用 Iterator
实现的,但是在 for-each
循环中直接调用 list.remove()
修改了列表的结构,ArrayList
的 modCount
值发生了变化,但 Iterator
中记录的初始 modCount
值没有同步更新,这就导致抛出了ConcurrentModificationException
。
注:虽然代码上看是 for (Integer num : list)
,但是底层实现实际上是通过隐式创建一个 Iterator
对 ArrayList
进行迭代的。等价代码如下:
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer num = iterator.next();
// Your logic
}
如果上面的原因还没看明白,我们继续往下看
三、list.remove(num)它是怎么删除的,为什么会使列表的结构修改?
大家可能还存在疑惑的地方:list.remove(num)它是怎么删除的,为什么会使列表的结构修改?
首先来看看ArrayList
中 remove(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
相等 的第一个元素。- 如果
o
是null
,它会找出第一个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
}
- 更新
modCount
modCount++
是 ArrayList
记录结构修改次数的变量。它会在任何结构性修改(如 add()
、remove()
)时增加,用于迭代器的并发修改检查。如果你在遍历过程中修改了 ArrayList
的结构(比如删除元素),****modCount
会与迭代器的初始状态不一致,导致抛出 ConcurrentModificationException
****。
- 执行删除操作
删除元素的核心是通过 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
位置的元素。
- 释放最后一个元素
数组中的最后一个元素会被设置为 null
,这样可以帮助垃圾回收器(GC)回收不再使用的对象。size--
减少了数组的大小,表示列表长度减少了。
1.所以总结下来整个删除流程
- 当调用
list.remove(num)
时,ArrayList
会遍历其内部的数组elementData
,寻找与num
相等 的元素。 - 找到后,调用
fastRemove(index)
,该方法通过System.arraycopy()
将index + 1
后面的元素向前移动一位,覆盖被删除的元素。 modCount
自增,通知迭代器有结构修改。- 将数组的最后一个元素设为
null
,并减少size
,从而完成删除操作。
2.所以list.remove(num)会使列表的结构修改的原因就在于:
在删除的时候, ArrayList
的 modCount
值发生了变化,但 Iterator
中记录的初始 modCount
值没有同步更新,这就导致抛出了 ConcurrentModificationException
。
四、正确方式是什么呢
可以使用 Iterator
的 remove()
方法来删除元素,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]
使用 Iterator
的 remove()
方法,同步更新 modCount
,就可以安全地修改列表结构了,同时避免 ConcurrentModificationException
。
如果有些宝子对迭代器的remove是怎么删除元素的感兴趣,可以接着往下看
五、Iterator.remove() 是怎么删除元素的呢?
Iterator.remove()
方法在 ArrayList
中的实现通过其内部类 Itr
来完成,该类实现了 Iterator
接口
当我们在迭代过程中调用 iterator.remove()
,它实际上会调用 Itr
类的 remove()
方法,该方法通过操作 ArrayList
的 remove()
来实现,并确保修改集合的同时保持迭代器的一致性。
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()
时,迭代器会检查当前的 modCount
和 expectedModCount
是否相等。如果不相等,说明集合在迭代过程中被修改了(而不是通过迭代器进行的安全修改),此时会抛出 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); // 不通过迭代器删除
}
}
可能出现的问题:
- 元素顺序问题:当你删除
3
后,后面的元素4
和5
会向前移动。此时迭代器的cursor
仍然指向下一个未删除的元素。然而,由于集合内部的元素已经移动,迭代器可能会跳过元素或重复访问同一个元素。- 比如:如果
cursor
还没更新,那么在删除3
后,迭代器会跳过4
,导致无法正确遍历所有元素。
- 比如:如果
- 访问无效元素:如果迭代器继续遍历,但集合已经发生了修改,它可能会尝试访问已经被删除的元素,导致程序崩溃或抛出
IndexOutOfBoundsException
。
看到这儿相信各位宝子应该就明白了,为什么不能list.remove修改列表的结构,而是采用迭代器的remove了吧。