ArrayList遍历过程中删除元素抛ConcurrentModificationException异常问题分析

现象

    List<String> list = new ArrayList<>();
    list.add("abc");
    list.add("bcd");
    list.add("efg");

    for (String e:list) {
        if ("abc".equals(e)) {
            list.remove(e);
        }
    }

在foreach遍历列表的过程中有删除操作会产生如下报错

java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)

原因分析

foreach编译后实际是采用迭代器来遍历列表,编译后的class文件内容如下:

        List<String> list = new ArrayList();
        list.add("abc");
        list.add("bcd");
        list.add("efg");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String e = (String)var2.next();
            if ("abc".equals(e)) {
                list.remove(e);
            }
        }

从上面的class字节码也可以看出foreach转化成了迭代器来遍历列表,从报错栈可以看到ConcurrentModificationException异常是在ArrayList的迭代器的next函数中出,我们跟进这个函数中看看其中的代码逻辑是怎样的

        public E next() {
            //1、modCount != expectedModCount时抛异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            int i = cursor;
            if (i >= limit)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            //2、当前元素索引大于等于数组长度时抛异常
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

该函数中有两处会抛ConcurrentModificationException异常,第2处是多线程的情况下访问ArrayLis会走到,我们主要分析第1处异常是如何抛出的。
第一处异常抛出的条件是modCount != expectedModCount,这两个变量代表什么含义,分别在什么情况下会赋值,搞清楚这两个问题,这个判断条件满足的场景也就清楚了。modCount定义在ArrayList的父类AbstractList中,代表列表被修改的次数,在列表元素数量发生变化的时候该值会发生改变,列表中增加元素或删除元素都会导致该值发生变化,而expectedModCount定义在ArrayList的内部迭代器类中,用于判断遍历的过程中ArrayList是否发生变化,此处的变化是指导致modCount变化的场景。
因此,在用迭代器遍历ArrayList的时候,ArrayList有发生变化的操作,则会抛ConcurrentModificationException异常。此处有一点需要注意,使ArrayList发生变化的操作都是指ArrayList中的方法,如remove、add、clear等方法,而迭代器中有个remove方法,也会导致ArrayList发生变化,但调用该迭代器的remove方法并不会导致抛异常,原因是迭代器中的remove方法在删除元素后会将expectedModCount赋值为modCount,这样就会保证下次再调用到next方法时不会触发抛异常的逻辑。

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            //此处在remove元素后将expectedModCount赋值为modCount
            expectedModCount = modCount;
            limit--;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

总结

1)在使用foreach方式遍历ArrayList时,实际采用的是迭代器的遍历方式,在遍历的过程中,迭代器的next方法会判断遍历的过程中ArrayList是否发生变化(此处的变化是指ArrayList的元素个数发生变化),如果发生了变化则会抛ConcurrentModificationException,这种方式遍历的过程中如果调用ArrayList的如下方法均会产生此异常:add、addAll、remove、removeAll、clear、retainAll。这里有个前提,调用完这些方法会继续遍历的过程,如果上面的方法
调用后就跳出了遍历过程则不会抛异常,因为没有再走到next方法,抛异常的条件也就不会触发。
2)如果想在遍历ArrayList的过程中改变ArrayList,可使用常规的for循环来遍历(注意正序遍历删除元素会漏删,原因和解决方案可参考此链接),如下:

    List<String> list = new ArrayList<>();
    list.add("abc");
    list.add("bcd");
    list.add("efg");

    for (int i = 0; i < list.size(); i++) {
        String e = list.get(i);
        if ("abc".equals(e)) {
            list.remove(e);
        }
    }

如果遍历的过程中仅仅是删除元素,也可以使用迭代器来实现,注意删除的时候使用的是迭代器的删除方法,如过使用ArrayList的remove方法依然会抛异常,如下

    List<String> list = new ArrayList<>();
    list.add("abc");
    list.add("bcd");
    list.add("efg");

    Iterator it = list.iterator();
    while (it.hasNext()) {
        String e = (String)it.next();
        if ("abc".equals(e)) {
            //使用迭代器的删除方法
            it.remove();
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值