使用 增强for循环
来删除List中的元素时报错, ConcurrentModificationException
@Test
public void test_1() {
ArrayList<String> strings = new ArrayList<>(List.of(new String[]{"AA", "BB", "test", "test1", "test2"}));
for (String string : strings) {
if (string.startsWith("t"))
strings.remove(string);
}
}
原因分析
跳转至ArrayList源码中定位异常信息。发现抛出异常的原因是因为modCount和expectedModCount不一致造成的。
// ArrayList.java 1095
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
modCount
的作用是用来跟踪结构修改的次数,主要体现在迭代器和并发修改检测上:
- 迭代器的快速失败机制(Fail-Fast):当通过迭代器遍历列表时,迭代器会持有迭代开始时的
modCount
的值。如果在迭代过程中发现列表的结构发生了修改(例如增加、删除元素。修改元素不会),则迭代器会检查当前的modCount
是否与迭代开始时的值相同,如果不同,则抛出ConcurrentModificationException
异常,以防止迭代器继续操作可能导致不确定行为的已修改列表。 - 并发修改检测:在多线程环境下,如果一个线程正在遍历列表,而另一个线程在同时修改列表结构,
modCount
可以帮助检测到这种并发修改的情况,从而避免可能的数据不一致性。
expectedModCount
是 Java 集合框架中一种用于支持快速失败(Fail-Fast)机制的标记,通常在迭代器的内部实现中使用。
在迭代器进行迭代过程中,每次对列表进行结构修改(比如添加、删除元素。修改元素不会)时,modCount
会自增,而expectedModCount
会在迭代器创建时记录下当前列表的 modCount
值。当迭代器通过 next()
方法遍历元素时,它会先检查 expectedModCount
是否与当前 modCount
相等,如果不相等,说明列表的结构发生了修改,则迭代器会抛出一个 ConcurrentModificationException
异常,避免可能导致不确定行为的情况发生。
因此,expectedModCount
的主要作用是帮助迭代器在迭代过程中检测到并发修改,实现快速失败机制,确保迭代器操作的安全性。它不是 Java 集合框架的核心概念,而是在一些具体的集合类(如 ArrayList
、HashMap
等)的迭代器实现中使用的。
在test中删除元素使用的是ArrayList.remove()
方法,这个方法的本质是调用的fastRemove()
方法。具体源码如下,可以看到的是fastRemove()
方法只对modCount++
并没有增加expectedModCount
属性、,自然会报错。
//ArrayList.java 719~730
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
解决方案
方案一 使用Iterator.remove():
@Test
public void test_2() {
ArrayList<String> strings = new ArrayList<>(List.of(new String[]{"AA", "BB", "test", "test1", "test2"}));
for (Iterator<String> itr = strings.iterator(); itr.hasNext();) {
if (itr.next().startsWith("t"))
// 注意这里是itr.remove(),而不是strings.remove()
itr.remove();
}
System.out.println(strings);
}
增强for循环也是使用的iterator,为什么这么写就不会报错呢?其核心在于删除使用的itr.remove();
而不是 strings.remove(string);
,前者是ArrayList下的一个内部类itr
它会同步modCount
给expectedModCount
后者不会。
//ArrayList.java 1060~1073
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; //同
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
方案二 使用Stream过滤:
@Test
public void test_3() {
ArrayList<String> strings = new ArrayList<>(List.of(new String[]{"AA", "BB", "test", "test1", "test2"}));
List<String> t = strings.stream().filter(s -> !s.startsWith("t")).collect(Collectors.toList());
System.out.println(t);
}
总结
在实际工作中,如果遇到了要删除ArrayList中的某些元素的需求时,一般不会选择删除元素,而是会new一个新List,把不需要删除的元素放进去,再用新List替换旧的List,就和最后一种方法一样。选择这样做的目的倒不是因为可以用拉姆达表达式的简单写法,而是因为这样效率更高。我们知道ArrayList删除元素实际上是创建了一个新的数组代替原来的数组,也就是每删除一次元素就会进行一次ArrayCopy,如果要删除的元素很多的话,效率就会很差,所以new新List的方法其实是效率更高的。
实际工程中,这种“遍历数组并且删除符合条件的元素”的需求,都应该使用最后一种方法来做。用迭代器确实是正确的方法,但不是最佳实践。