为什么iterator.remove不会抛出ConcurrentModificationException异常

24
iterator.remove()list.remove() 有什么不同之处,以至于迭代器不会抛出异常而 list.remove() 会抛出异常?最终,两者都会修改集合大小。

请忽略多线程。我只是在谈论一个 for-each 循环和一个迭代器循环。据我所知,for-each 循环仅在内部创建迭代器。

我感到困惑。

6个回答

49

我想你的意思是,如果你正在迭代一个列表,为什么使用list.remove()会导致抛出ConcurrentModificationExceptioniterator.remove()则不会呢?

考虑以下示例:

    List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));

    for (Iterator<String> iter = list.iterator(); iter.hasNext(); ) {
        if (iter.next().equals("b")) {
            // iter.remove();    // #1
            // list.remove("b"); // #2
        }
    }

如果你取消注释第一行,它将正常工作。如果你取消注释第二行(但注释掉第一行),那么后续对iter.next()的调用将导致抛出ConcurrentModificationException异常。

原因是迭代器是一个单独的对象,它具有对基础列表内部状态的一些引用。如果你在迭代器正在操作时修改了列表,可能会导致迭代器表现不良,例如跳过元素、重复元素、超出数组末尾索引等。它试图检测这种修改,如果发现就会抛出ConcurrentModificationException异常。

通过迭代器删除元素可以正常工作且不会导致异常,因为这会更新基础列表引用内部列表的迭代器状态,从而使所有内容保持一致。

然而,iterator.remove()并没有特别之处,可以让它在所有情况下正常工作。如果有多个迭代器遍历同一个列表,由一个迭代器所做的修改将会给其他迭代器带来问题。考虑以下情况:

    Iterator<String> i1 = list.iterator();
    Iterator<String> i2 = list.iterator();
    i1.remove();
    i2.remove();

我们现在有两个指向同一列表的迭代器。如果我们使用其中一个来修改列表,则会干扰第二个的操作,因此调用 i2.remove() 将导致 ConcurrentModificationException 异常。


4
这个实际上是更合适的答案。 - Andrii Plotnikov
这是最好的解释。 - AnirbanDebnath
1
好的解释。这应该是正确的答案。 - Saddam Pojee
1
这是对行为实现(在各种集合类中)的解释。但是行为的原因是Iterator类被设计成以这种方式工作...并且规定以这种方式工作。设计/规范导致了实现,而不是相反。 - Stephen C
1
@StephenC 在这种情况下,设计和规范是基于实现考虑而得出的。当然,迭代器被设计和指定为按照它的方式工作...但是为什么呢?答案是在可预测性、可靠性、易于实现和易于使用之间进行权衡。可以很容易地考虑具有更强保证的规范,但这将对实现施加繁重的要求。我可以进一步解释,但这个评论已经变得很长了。如果您需要额外的解释,请随时提问。 :-) - Stuart Marks

17
ConcurrentModificationException 不会被 Iterator.remove() 抛出,因为这是迭代过程中修改集合的允许方式。这是 Iteratorjavadoc 中所述的:

从基础集合中删除此迭代器最后一个返回的元素(可选操作)。每次调用 next() 方法只能调用一次此方法。如果通过除此方法以外的任何方式修改正在进行迭代的基础集合,则迭代器的行为是未指定的。

如果您以其他方式更改正在迭代的集合,则可能会根据迭代器的实现和您正在迭代的集合(或其他内容)而得到异常。(某些集合类不会给您抛出 ConcurrentModificationException:请查看各自的javadoc,了解它们迭代器的行为如何指定)

如果您在同一集合上有两个迭代器,并且通过其中一个删除,则也可能会得到异常。


与 list.remove 相比,iterator.remove 有什么不同之处,iterator 不会抛出异常而 list.remove 会抛出?

原因 #1. 如果你同时从同一调用堆栈的两个位置更新非并发集合,那么行为将会破坏迭代的设计不变式1。 对非并发集合的迭代保证恰好能够看到集合中所有元素各一次。(相比之下,并发集合的这些保证是放松的)。

原因 #2. 非并发集合类型没有实现为线程安全。 因此,如果使用集合和迭代器来由不同的线程更新集合,则可能存在竞争条件和内存异常。 这不是一个强有力的原因,因为您无论如何都会遇到这些问题。 但是,以两种不同的方式进行更新会使问题变得更糟。


我只是在谈论 for-each 循环和 iterator 循环。 据我所知,for-each 循环在内部仅创建迭代器。

没错,for-each 循环只是使用迭代器的 while 循环的语法糖。

另一方面,如果您使用以下循环:

    for (int i = 0; i < list.size(); i++) {
        if (...) {
            list.remove(i);
        }
    }

你不会遇到ConcurrentModificationException,但你需要调整删除元素的索引变量,并且其他线程的更新可能会导致你跳过某些元素或多次访问它们2


1 - 为了实现“恰好一次”迭代行为,当您通过集合对象删除元素时,迭代器数据结构需要更新以使其与已发生的更改保持同步。由于当前的实现不保留与未完成的迭代器的链接,因此这是不可能的。而如果它们这样做,它们将需要使用Reference对象或面临内存泄漏的风险。

2 - 或甚至获得IndexOutOfBoundsException。如果集合不是并发/正确同步的,你可能会遇到更严重的问题。


4

因为是迭代器抛出异常。如果调用 List.remove(),列表不知道元素已被移除,只知道某些东西在它的“脚下”发生了变化。如果调用 Iterator.remove(),则知道当前元素已被移除及如何处理。


0
public class ArrayListExceptionTest {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();
        list1.add("a");
        list1.add("b");
        list1.add("c");
        Iterator<String> it1 = list1.iterator();
        ArrayList<String> list2 = new ArrayList<String>();
        list2.add("a");
        try {

            while (it1.hasNext()) {
                list1.add(it1.next());
            }
        } catch (ConcurrentModificationException e) {
            e.printStackTrace();
        }
        it1 = list1.iterator();
        while (it1.hasNext()) {
            System.out.println(it1.next());
        }
        it1 = list1.iterator();
        try {
            while (it1.hasNext()) {
                if (it1.next().equals("a"))
                    list1.retainAll(list2);
            }

        } catch (ConcurrentModificationException e) {
            e.printStackTrace();
        }
        it1 = list1.iterator();
        while (it1.hasNext()) {
            System.out.println(it1.next());
        }
        it1 = list1.iterator();
        Iterator<String> it2 = list1.iterator();
        it1.remove();
        it2.remove();
    }
}

您可以看到上述3种情况:

情况1:通过添加元素进行修改,因此在使用next()函数时会导致ConcurrentModificationException异常。

情况2:通过使用retain()进行修改,因此在使用next()函数时会导致ConcurrentModificationException异常。

情况3:将抛出java.lang.IllegalStateException而不是ConcurrentModificationException异常。

输出:

a
b
c
a

a
a

    java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
        at java.util.ArrayList$Itr.next(ArrayList.java:859)
        at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:21)
    java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
        at java.util.ArrayList$Itr.next(ArrayList.java:859)
        at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:37)
    Exception in thread "main" java.lang.IllegalStateException
        at java.util.ArrayList$Itr.remove(ArrayList.java:872)
        at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:55)

0
回答这个问题并提供一些低级细节:
在迭代过程中,当调用next()方法时,会抛出ConcurrentModificationException异常。
因此,不是集合的remove()方法引发了此异常,而是迭代器实现的next()方法。
Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
    at Collection.IteratorDemo.main(IteratorDemo.java:16)

你可以在上面的错误日志中检查第三行。

  List<Integer> nums = new ArrayList<>();
     nums.add(1);
     nums.add(2);
     for(int i : nums){
        nums.remove(1);
        System.out.println(i);
 }

这个 next() 方法如何知道集合是否被修改了?
通过检查一个变量,AbstractList
 protected transient int modCount = 0;

这个变量通过在添加/删除集合调用中增加和减少值来维护集合的结构更改。这就是集合实现快速失败迭代器的方式。


0

这里举个例子,如果集合迭代器没有检查底层集合的修改情况,事情可能会出错。这就是 ArrayList 迭代器的实现方式:

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

    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size) throw new NoSuchElementException();
        // ...
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        // ...
        ArrayList.this.remove(lastRet);
        // ...
        cursor = lastRet;
        lastRet = -1;
    }

让我们看一个例子:

List list = new ArrayList(Arrays.asList(1, 2, 3, 4));
Iterator it = list.iterator();
Integer item = it.next();

我们移除了第一个元素

list.remove(0);

如果我们现在想要调用 it.remove(),迭代器将会移除 number 2,因为这是字段 lastRet 现在所指向的内容。
if (item == 1) {
   it.remove(); // list contains 3, 4
}

这将是不正确的行为!迭代器的契约规定remove()删除next()返回的最后一个元素,但在存在并发修改的情况下,它无法保持其契约。因此,它选择保守起见并抛出异常。

对于其他集合,情况可能更加复杂。如果您修改了HashMap,它可能会根据需要增长或缩小。此时,元素将落入不同的桶中,并且在重新哈希之前保持指向桶的迭代器将完全丢失。

请注意,iterator.remove()本身不会抛出异常,因为它能够更新自身和集合的内部状态。但是,在同一实例集合的两个迭代器上调用remove()将引发异常,因为它会使其中一个迭代器处于不一致状态。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接