map list详解工作需要的技能

ArrayList不能用foreach增删改元素

2019-11-13  本文已影响0人  Djbfifjd

一、快速失败(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 进行加锁操作。

上一篇下一篇

猜你喜欢

热点阅读