美文网首页map list详解工作需要的技能
ArrayList不能用foreach增删改元素

ArrayList不能用foreach增删改元素

作者: Djbfifjd | 来源:发表于2019-11-13 07:05 被阅读0次

    一、快速失败(fail-fast)

    在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException

    1️⃣原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

    2️⃣注意:这里异常的抛出条件是检测到modCount!=expectedmodCount这个条件。如果集合发生变化时,修改 modCount 值刚好又设置为 expectedmodCount 值,则不会抛出异常。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。

    3️⃣场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

    二、安全失败(fail-safe)

    采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

    1️⃣原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。

    2️⃣缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

    3️⃣场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

    三、引出问题

    请看下面两段代码,哪段代码会报错?

    List<String> listA = new ArrayList<String>();
       listA.add("1");
       listA.add("2");
    for (String s : listA) {
       if("1".equals(s)){
        listA.remove(s);
        }
    }
    

    1️⃣listA的remove方法成功执行。

    List<String> listB = new ArrayList<String>();
        listB.add("2");
        listB.add("1");
    for (String s : listB) {
        if("1".equals(s)){
            listB.remove(s);
        }
    }
    

    2️⃣listB的remove方法运行抛出ConcurrentModificationException异常。

    四、为了寻找原因,查看源码

    增强 for 循环,其实是依赖了 while 循环和 Iterator 实现的。所有的 Collection 集合类都会实现 Iterable 接口。找到 ArrayList 类的 Iterator():使用自己的 Itr 内部类,并且实现了 Iterator 接口。

    迭代器的本质是先调用 hasNext() 判断是否存在下一个元素,然后再使用 next() 取下一个元素:Itr 内部类实现

    /**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = 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);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
    
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
    

    1️⃣listA 能 remove 成功,是因为它只循环了一次。因为它在 remove 元素 1 之后,它的 size - 1 变成 1,然后 Itr 内部的 cursor 变量由 0 变成 1。此时 1 = 1,循环结束,所以成功了。
    2️⃣listB 却 remove 失败,其实它在循环第二次的时候,也 remove 成功了,但是第三次判断 next 的时候 cursor 的值为 2 导致不等于现在的 size 1,所以执行了 next 方法。最重要的来了,之前 remove 的操作导致 ArrayList 的 modCount 值加 1,然后 Itr 类中的 expectedModCount 保持不变,所以会抛出异常。

    同理可得,由于 add 操作也会导致 modCount 自增,所以不允许在 foreach 中增删改 ArrayList 中的元素。对此,推荐使用迭代器 Iterator 删除元素:

    Iterator<String> iterator = arrayList2.iterator();
    while(iterator.hasNext()){
        String item = iterator.next();
        if("1".equals(item)){
            iterator.remove();
        }
    }
    

    如果存在并发操作,还需要对 Iterator 进行加锁操作。

    相关文章

      网友评论

        本文标题:ArrayList不能用foreach增删改元素

        本文链接:https://www.haomeiwen.com/subject/qxwcictx.html