【问题标题】:Collection throws or doesn't throw ConcurrentModificationException based on the contents of the Collection [duplicate]集合根据集合的内容抛出或不抛出 ConcurrentModificationException [重复]
【发布时间】:2026-01-21 17:30:01
【问题描述】:

以下 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 而第一个例子呢?

【问题讨论】:

    标签: java concurrentmodification


    【解决方案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();
        }
    }
    

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

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

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

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

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

    编辑添加:

    This question 提供了一些关于为什么并发修改检查hasNext() 中执行并且仅在next() 中执行的信息。

    【讨论】:

    • 在这两种情况下删除功能是相同的。我的问题是为什么它在第二种情况下没有给出错误,但在第一种情况下它给出了错误?
    • 实际上,删除哪个元素并不重要,重要的是当前元素是什么,即迭代器的位置。除倒数第二个以外的任何位置都会引发异常,但如果迭代器位于倒数第二个元素上并且您删除了 any 元素,则迭代将停止而不会出现错误,并且不会迭代最后一个元素。
    • 但是集合被改变的那一刻不应该在那一刻给出一个例外吗?我不知道 hasext() 、next() 和 remove(Object o) 背后的代码。你能解释一下这些代码的实现吗?
    • @Andreas 你是对的。通常的错误是尝试通过集合而不是通过迭代器删除“当前”元素,所以我会坚持下去。
    • @RaviKuldeep 修改集合时不可能引发异常,因为集合不知道任何现有的迭代器(即使它跟踪创建的迭代器),因此检查修改的唯一可能点是访问迭代器的时间。可以签入Iterator.hasNext(),但 Java 出于某种原因不这样做。
    【解决方案2】:

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

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

    hasNext() 只是:

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

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

    哎呀!!!!

    【讨论】:

    • 我检查了haveext()、next() 和remove(Object o) 的实现,但不能完全理解它们。你能解释一下关于这些实现的答案吗?
    • @RaviKuldeep 我展示了hasNext() 的实现,并解释了它是如何/为什么失败的。我不打算详细解释整个迭代器类是如何工作的。如果您看不懂代码,请接受正确使用它可以工作的事实,并且不保证错误使用时会失败。以后等对Java比较了解了,可以再看一遍。
    【解决方案3】:

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

    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() 返回 false 并且永远不会执行 i.next() 以引发异常

    【讨论】:

      【解决方案4】:

      您应该直接从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.
          }
      }
      

      编辑:

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

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

      【讨论】:

      • 在这两种情况下删除功能是相同的。我的问题是为什么它在第二种情况下没有给出错误,但在第一种情况下它给出了错误?
      • @RaviKuldeep 我已经更新了我的答案
      • 但是集合被改变的那一刻不应该在那一刻给出一个例外吗?我不知道 hasext() 、next() 和 remove(Object o) 背后的代码。能否请您解释一下这些代码的实现。如果我理解错误,请纠正我
      • @RaviKuldeep 不,这个问题是由于循环而出现的,尝试使用 3 个元素的第二种方法,你应该得到同样的错误
      【解决方案5】:

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

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

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

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

      【讨论】: