使用 `增强for循环` 来删除List中的元素时报错, `ConcurrentModificationException`

使用 增强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 的作用是用来跟踪结构修改的次数,主要体现在迭代器和并发修改检测上:

  1. 迭代器的快速失败机制(Fail-Fast):当通过迭代器遍历列表时,迭代器会持有迭代开始时的 modCount 的值。如果在迭代过程中发现列表的结构发生了修改(例如增加、删除元素。修改元素不会),则迭代器会检查当前的 modCount 是否与迭代开始时的值相同,如果不同,则抛出 ConcurrentModificationException 异常,以防止迭代器继续操作可能导致不确定行为的已修改列表。
  2. 并发修改检测:在多线程环境下,如果一个线程正在遍历列表,而另一个线程在同时修改列表结构,modCount 可以帮助检测到这种并发修改的情况,从而避免可能的数据不一致性。

expectedModCount 是 Java 集合框架中一种用于支持快速失败(Fail-Fast)机制的标记,通常在迭代器的内部实现中使用。
在迭代器进行迭代过程中,每次对列表进行结构修改(比如添加、删除元素。修改元素不会)时,modCount 会自增,而expectedModCount 会在迭代器创建时记录下当前列表的 modCount 值。当迭代器通过 next() 方法遍历元素时,它会先检查 expectedModCount 是否与当前 modCount 相等,如果不相等,说明列表的结构发生了修改,则迭代器会抛出一个 ConcurrentModificationException 异常,避免可能导致不确定行为的情况发生。

因此,expectedModCount 的主要作用是帮助迭代器在迭代过程中检测到并发修改,实现快速失败机制,确保迭代器操作的安全性。它不是 Java 集合框架的核心概念,而是在一些具体的集合类(如 ArrayListHashMap 等)的迭代器实现中使用的。

在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它会同步modCountexpectedModCount后者不会。

//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的方法其实是效率更高的。
实际工程中,这种“遍历数组并且删除符合条件的元素”的需求,都应该使用最后一种方法来做。用迭代器确实是正确的方法,但不是最佳实践。

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值