集合基于集合的内容引发或不抛出 ConcurrentModificationException

2022-09-03 00:22:48

以下 Java 代码按预期抛出 一个 :ConcurrentModificationException

public class Evil
{
    public static void main(String[] args) {
        Collection<String> c = new ArrayList<String>();
        c.add("lalala");
        c.add("sososo");
        c.add("ahaaha");
        removeLalala(c);
        System.err.println(c);
    }
    private static void removeLalala(Collection<String> c) 
    {
        for (Iterator<String> i = c.iterator(); i.hasNext();) {
            String s = i.next();
            if(s.equals("lalala")) {
                c.remove(s);
            }
        }
    }
}

但是以下示例(仅在 的内容上有所不同)执行时没有任何异常:Collection

public class Evil {
    public static void main(String[] args) 
    {
        Collection<String> c = new ArrayList<String>();
        c.add("lalala");
        c.add("lalala");
        removeLalala(c);
        System.err.println(c);
    }
    private static void removeLalala(Collection<String> c) {
        for (Iterator<String> i = c.iterator(); i.hasNext();) {
            String s = i.next();
            if(s.equals("lalala")) {
                c.remove(s);
            }
        }
    }
}

这将打印输出“[lalala]”。为什么第二个示例不抛出 a,而第一个示例却抛出 a?ConcurrentModificationException


答案 1

简短的回答

因为不能保证迭代器的快速失败行为。

长答案

您之所以收到此异常,是因为在循环访问集合时无法操作该集合,除非通过迭代器。

坏:

// we're using iterator
for (Iterator<String> i = c.iterator(); i.hasNext();) {  
    // here, the collection will check it hasn't been modified (in effort to fail fast)
    String s = i.next();
    if(s.equals("lalala")) {
        // s is removed from the collection and the collection will take note it was modified
        c.remove(s);
    }
}

好:

// we're using iterator
for (Iterator<String> i = c.iterator(); i.hasNext();) {  
    // here, the collection will check it hasn't been modified (in effort to fail fast)
    String s = i.next();
    if(s.equals("lalala")) {
        // s is removed from the collection through iterator, so the iterator knows the collection changed and can resume the iteration
        i.remove();
    }
}

现在进入“为什么”:在上面的代码中,请注意修改检查是如何执行的 - 删除将集合标记为已修改,下一个迭代检查是否有任何修改,如果它检测到集合已更改,则会失败。另一件重要的事情是(不确定其他集合)不会检查 中的修改。ArrayListhasNext()

因此,可能会发生两件奇怪的事情:

  • 如果在迭代时删除最后一个元素,则不会引发任何内容
    • 这是因为没有“next”元素,因此迭代在到达修改检查代码之前结束。
  • 如果删除倒数第二个元素,实际上也会返回 ,因为迭代器现在指向最后一个元素(以前的倒数第二个元素)。ArrayList.hasNext()falsecurrent index
    • 因此,即使在这种情况下,删除后也没有“下一个”元素

请注意,这一切都符合ArrayList的文档

请注意,迭代器的故障快速行为无法得到保证,因为一般来说,在存在不同步并发修改的情况下,不可能做出任何硬保证。快速失败迭代器在尽力而为的基础上抛出 ConcurrentModificationException。因此,编写一个依赖于此异常的正确性的程序是错误的:迭代器的故障快速行为应该仅用于检测错误。

编辑以添加:

此问题提供了有关为什么不在 中执行并发修改检查而仅在 中执行的一些信息。hasNext()next()


答案 2

如果您查看迭代器(私有嵌套类)的源代码,您将看到代码中的缺陷。ArrayListItr

代码应该是快速失败的,这是在迭代器内部通过调用来完成的,但是不会进行该调用,可能是出于性能原因。checkForComodification()hasNext()

相反,它只是:hasNext()

public boolean hasNext() {
    return cursor != size;
}

这意味着,当您位于列表的倒数第二个元素上,然后删除一个元素(任何元素)时,大小会减小,并认为您位于最后一个元素上(您不是),并返回 ,跳过最后一个元素的迭代而不会出错。hasNext()false

哎呀!!!!


推荐