Collection 抛出或不抛出 ConcurrentModificationException 基于 Collection 的内容

Collection throws or doesn't throw ConcurrentModificationException based on the contents of the Collection

以下 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]”。为什么第二个示例在第一个示例抛出 ConcurrentModificationException 时不抛出?

您应该从 iterator (i) 中删除,而不是直接从 collection (c) 中删除;

试试这个:

for (Iterator<String> i = c.iterator(); i.hasNext();) {
    String s = i.next();
    if(s.equals("lalala")) {
        i.remove(); //note here, changing c to  i with no parameter.
    }
}

编辑:

第一次尝试抛出异常而第二次尝试抛出异常的原因仅仅是因为您的集合中的元素数量。

因为第一个会多次循环,而第二个只会迭代一次。因此,它没有机会抛出异常

简答

因为无法保证迭代器的快速失败行为。

长答案

您遇到此异常是因为您无法在迭代集合时操作集合,除非通过迭代器。

差:

// 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();
    }
}

现在 "why":在上面的代码中,请注意修改检查是如何执行的 - 删除将集合标记为已修改,下一次迭代检查是否有任何修改,如果检测到集合已更改则失败.另一个重要的事情是ArrayList(不确定其他集合)检查hasNext()中的修改。

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

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

注意这一切都符合ArrayList's documentation:

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

编辑添加:

提供了一些信息,说明为什么并发修改检查 不是 hasNext() 中执行并且仅在 next() 中执行。

从其他答案中,您知道在迭代集合时删除集合中元素的正确方法是什么。 我在这里给出基本的解释question.And你的问题的答案在下面的堆栈跟踪

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
    at java.util.ArrayList$Itr.next(Unknown Source)
    at com.ii4sm.controller.Evil.removeLalala(Evil.java:23)
    at com.ii4sm.controller.Evil.main(Evil.java:17)

在堆栈跟踪中,很明显 i.next(); 行抛出了错误。但是当集合中只有两个元素时。

Collection<String> c = new ArrayList<String>();
c.add("lalala");
c.add("lalala");
removeLalala(c);
System.err.println(c);

当第一个被移除时i.hasNext() returns false并且i.next()永远不会执行抛出异常

如果您使用 "for each" 循环浏览,则无法从列表中删除。

您不能从正在迭代的集合中删除项目。您可以通过显式使用 Iterator 并删除那里的项目来解决这个问题。您可以使用迭代器。

如果你使用下面的代码,你不会得到任何异常:

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);
      }
    }*/

    Iterator<String> it = c.iterator();
    while (it.hasNext()) {
        String st = it.next();
        if (st.equals("lalala")) {
            it.remove();
        }
    }
  }

如果您查看 ArrayList 迭代器的源代码(私有嵌套 class Itr),您会发现代码中的缺陷。

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

hasNext() 只是:

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

这意味着当你在列表的倒数第二个元素上,然后删除一个元素(任何元素)时,大小会减小并且hasNext() 认为你在最后一个元素上(你不是),并且 returns false,跳过最后一个元素的迭代而没有错误。

哎呀!!!!