【问题标题】:Why does calling remove() on an iterator give a ConcurrentModificationException?为什么在迭代器上调用 remove() 会产生 ConcurrentModificationException?
【发布时间】:2012-04-02 03:21:20
【问题描述】:

我正在尝试编写一个非常简单的方法来删除 LinkedList 中的重复项:

我尝试在不使用额外缓冲区的情况下执行此操作,因此我在链表上维护了两个迭代器,一个进行正常迭代,另一个迭代所有先前的节点以检查是否存在欺骗(如 CareerCup 中所示);然而,编译器告诉我有一个 CME,即使我正在调用 itr1.remove():

public static void RemoveWithoutBuffer(LinkedList l) {
    ListIterator itr1 = l.listIterator();   
    int count1 = 0;
    int count2 = 0;
    while (itr1.hasNext()) {
        Object next = itr1.next();
        count1++;
        count2 = 0;
        ListIterator itr2 = l.listIterator();
        while (itr2.hasNext()) {
            count2++;
            if (count2 == count1)
                break;
            if (itr2.next() == next){
                itr1.remove();
            }
        }

    }
}

借助 hashset 的另一个更简单的解决方案如下,并且没有报告异常:

    public static void Remove(LinkedList l) {
    HashSet set = new HashSet();
    ListIterator itr = l.listIterator();
    while (itr.hasNext()) {
        Object next = itr.next();
        if (set.contains(next))
            itr.remove();
        else
            set.add(next);
    }
}

是不是因为当我迭代 itr2 时,我无法在 itr1 上进行修改?有没有办法解决这个问题?谢谢各位。

【问题讨论】:

    标签: java iterator linked-list


    【解决方案1】:

    在第一种情况下是的 - 您正在通过 iterator2 更改集合的内容,而 iterator1 不知道这些更改。在第二种情况下,HashSet/HashMap 不允许在遍历元素时移除元素。

    您可以将已删除的元素添加到另一个集合中,并在迭代后将它们全部删除。例如

        List toRemove = new ArrayList();
        for (Object next : collection) {
            if (someCondition) toRemove.add(next);
        }
        collection.removeAll(toRemove);
    

    希望对你有帮助。

    附:有关如何从列表中删除元素的更多详细信息,关于算法复杂性,您可以在此处阅读Removing ArrayList object issue

    【讨论】:

    • 谢谢。该解决方案很棒,但仍会消耗一些额外的空间,尽管我猜它与使用 hashSet 相比节省了更多空间。也感谢您的链接。
    • 如果担心空间问题,您可以将元素标记为已删除(即,将列表设置为 null,或使用具有“isRemoved”属性的特殊键用于集合并稍后执行实际删除)。
    【解决方案2】:

    来自the API docs

    这个类的iteratorlistIterator返回的迭代器 方法是fail-fast:如果列表在任何结构上被修改 迭代器创建后的时间,除了通过 迭代器自己的removeadd方法,迭代器会抛出一个 ConcurrentModificationException.

    【讨论】:

      【解决方案3】:

      您获得 CME 是因为列表已被第二个迭代器修改,而第一个迭代器不知道该更改。下次它尝试访问该列表时,它已经被修改了。因此它会抛出异常。

      请一次只使用一个迭代器来修改列表。

      【讨论】:

        【解决方案4】:

        是的。你可以这样想:当你创建一个迭代器时,它会获取列表的当前“修改计数”。当迭代器从列表中删除一个元素时,它会检查修改计数以查看它是否符合预期,如果一切正常,它会删除该元素并更新迭代器和列表的修改计数。另一个迭代器仍将具有旧的修改计数并看到新值并抛出 CME。

        在大多数情况下,基于哈希集的方法是正确的解决方案。它将执行得更好——两次 O(n) 次通过通常比 O(n^2) 更好,因为嵌套迭代解决方案会产生(如果有效的话)。

        【讨论】:

        • 谢谢!所以我想没有办法真正使用 2 个迭代器来移除项目而不使用额外的空间? Eugene 给出的解决方案很棒,但仍然会占用一些额外的空间。是的,从时间上讲,这很糟糕,只是想尝试一下,以防有不使用临时缓冲区的要求。
        【解决方案5】:

        其他人解释了您获得 CME 的原因,这是您可以用来删除重复的可能方法

        使用 List toRemove 第一次记录元素 iterator 偶然发现它,然后当再次遇到记录的元素时,使用 iterator.remove() 将其删除

         private void removeDups(List list) {
                List toRemove = new ArrayList();
                for(Iterator  it = list.iterator(); it.hasNext();) {
                    Object next = it.next();
                    if(!toRemove.contains(next)) {
                        toRemove.add(next);
                    } else {
                        it.remove();
                    }
                }
                toremove.clear();
           } 
        

        【讨论】:

          【解决方案6】:

          是的,您可以在不使用额外空间的情况下解决此问题。

          问题来自于执行这两行代码,一个接一个。

          • itr1.remove();
          • itr2.hasNext();

          您可以同时在同一个列表上使用两个迭代器。 如果你小心点
          但是一旦其中一个迭代器修改了列表(就像 itr1.remove() 发生的那样),您就不能再使用另一个迭代器(因此您不能调用 itr2.hasNext())。

          解决方案是在itr1.remove() 之后放置一个break。并更新count1

          public static void RemoveWithoutBuffer(LinkedList l) {
              ListIterator itr1 = l.listIterator();   
              int count1 = 0;
              int count2 = 0;
              while (itr1.hasNext()) {
                  Object next = itr1.next();
                  count1++;
                  count2 = 0;
                  ListIterator itr2 = l.listIterator();
                  while (itr2.hasNext()) {
                      count2++;
                      if (count2 == count1)
                          break;
                      if (itr2.next() == next){
                          itr1.remove();
                          --count1;
                          break;
                      }
                  }
              }
          }  
          

          一个更优雅的解决方案是与当前元素之后的元素而不是当前元素之前的元素进行比较:

          public static void RemoveWithoutBuffer(LinkedList l) {
            ListIterator itr1 = l.listIterator();
            while (itr1.hasNext()) {
              Object next = itr1.next();
              ListIterator itr2 = l.listIterator( itr1.nextIndex() );
              while (itr2.hasNext()) {
                if (itr2.next() == next) {
                  itr1.remove();
                  break;
                }
              }
            }
          }
          

          就计算复杂性而言,这些并不是最佳解决方案。如果内存空间不是问题,hashset 解决方案在计算复杂度方面是更好的解决方案。

          但问题是关于通过迭代器进行并发修改,而不是关于复杂性优化。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-10-21
            • 2012-06-10
            • 1970-01-01
            • 2015-02-13
            • 1970-01-01
            • 1970-01-01
            • 2017-11-11
            • 2017-12-06
            相关资源
            最近更新 更多