题 迭代集合,在循环中删除时避免ConcurrentModificationException


我们都知道你不能这样做:

for (Object i : l) {
    if (condition(i)) {
        l.remove(i);
    }
}

ConcurrentModificationException 等等......这显然有时起作用,但并非总是如此。这是一些特定的代码:

public static void main(String[] args) {
    Collection<Integer> l = new ArrayList<Integer>();

    for (int i=0; i < 10; ++i) {
        l.add(new Integer(4));
        l.add(new Integer(5));
        l.add(new Integer(6));
    }

    for (Integer i : l) {
        if (i.intValue() == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

当然,这会导致:

Exception in thread "main" java.util.ConcurrentModificationException

...即使多线程没有这样做......无论如何。

什么是这个问题的最佳解决方案?如何在循环中从集合中删除项而不抛出此异常?

我也使用任意的 Collection 在这里,不一定是 ArrayList,所以你不能依赖 get


1029
2017-10-21 23:23


起源


读者注意:请阅读 docs.oracle.com/javase/tutorial/collections/interfaces/...,它可能有一种更简单的方法来实现你想做的事情。 - GKFX


答案:


Iterator.remove() 是安全的,你可以像这样使用它:

List<String> list = new ArrayList<>();

// This is a clever way to create the iterator and call iterator.hasNext() like
// you would do in a while-loop. It would be the same as doing:
//     Iterator<String> iterator = list.iterator();
//     while (iterator.hasNext()) {
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        // Remove the current element from the iterator and the list.
        iterator.remove();
    }
}

注意 Iterator.remove() 是迭代过程中修改集合的唯一安全方法;如果在迭代进行过程中以任何其他方式修改基础集合,则行为未指定。

资源: docs.oracle>集合接口


同样,如果你有一个 ListIterator 并希望  物品,你可以使用 ListIterator#add,出于同样的原因你可以使用 Iterator#remove - 它的设计允许它。


1468
2017-10-21 23:27



如果要删除当前迭代中返回的元素以外的元素,该怎么办? - Eugen
你必须在迭代器中使用.remove,并且只能删除当前元素,所以没有:) - Bill K
请注意,与使用ConcurrentLinkedDeque或CopyOnWriteArrayList相比,这会更慢(至少在我的情况下) - Dan
我曾经在数组上向后迭代并删除.. - morksinaanab
是不是可以把 iterator.next() 在for-loop中调用?如果没有,有人可以解释原因吗? - Blake


这有效:

Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
    if (iter.next().intValue() == 5) {
        iter.remove();
    }
}

我假设因为foreach循环是用于迭代的语法糖,使用迭代器无济于事......但是它给了你这个 .remove() 功能。


320
2017-10-21 23:26



foreach循环 是 用于迭代的句法糖。但是正如您所指出的,您需要在迭代器上调用remove - foreach不允许您访问。因此你无法在foreach循环中删除的原因(即使你 是 实际上在引擎盖下使用迭代器) - madlep
+1例如代码在上下文中使用iter.remove(),而Bill K的回答并没有[直接]。 - Eddified
我正在删除迭代器,但是我仍然会遇到同样的错误。任何想法? - Gokhan Arik


使用Java 8,您可以使用 新的 removeIf 方法。适用于您的示例:

Collection<Integer> coll = new ArrayList<Integer>();
//populate

coll.removeIf(i -> i.intValue() == 5);

156
2018-05-28 10:11



OOOOO!我希望Java 8或9中的某些内容可能有所帮助。这对我来说似乎相当冗长,但我仍然喜欢它。 - James T Snell
在这种情况下是否也推荐使用equals()? - Anmol Gupta
顺便一提 removeIf 使用 Iterator 和 while 循环。你可以在java 8上看到它 java.util.Collection.java - omerhakanbilici
@omerhakanbilici有些实现如 ArrayList 因性能原因而覆盖它。您指的是仅默认实现。 - Didier L


由于问题已经得到解答,即最好的方法是使用迭代器对象的remove方法,我会进入错误发生地点的具体情况 "java.util.ConcurrentModificationException" 被抛出。

每个集合类都有一个私有类,它实现了Iterator接口并提供了类似的方法 next()remove() 和 hasNext()

下一个代码看起来像这样......

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

这里的方法 checkForComodification 实现为

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

因此,正如您所看到的,如果您明确尝试从集合中删除元素。它导致了 modCount 与众不同 expectedModCount,导致例外 ConcurrentModificationException


38
2018-05-15 19:57



很有意思。谢谢!我经常不会自己调用remove(),而是在迭代之后更喜欢清除集合。不是说这是一个很好的模式,就像我最近一直在做的那样。 - James T Snell


你可以像你提到的那样直接使用迭代器,或者保留第二个集合并将要删除的每个项目添加到新集合中,然后在最后删除所有项目。这允许你继续使用for-each循环的类型安全性,代价是增加内存使用和cpu时间(除非你有真正的大型列表或真正的旧计算机,否则不应该是一个大问题)

public static void main(String[] args)
{
    Collection<Integer> l = new ArrayList<Integer>();
    Collection<Integer> itemsToRemove = new ArrayList<Integer>();
    for (int i=0; i < 10; ++i) {
    l.add(new Integer(4));
    l.add(new Integer(5));
    l.add(new Integer(6));
    }
    for (Integer i : l)
    {
        if (i.intValue() == 5)
            itemsToRemove.add(i);
    }

    l.removeAll(itemsToRemove);
    System.out.println(l);
}

22
2017-10-21 23:32



这就是我通常所做的,但是显式迭代器是我感觉更精确的解决方案。 - Claudiu
很公平,只要你没有用迭代器做任何事情 - 让它暴露使得更容易做每次循环调用.next()两次等事情。这不是一个大问题,但如果你做的话可能会导致问题比通过列表删除条目更复杂的事情。 - RodeoClown
@RodeoClown:在最初的问题中,Claudiu正在从Collection中删除,而不是迭代器。 - matt b
从迭代器中删除会从底层集合中删除...但是我在上一条评论中说的是,如果你正在做的事情比仅仅使用迭代器查找循环中的删除(比如处理正确的数据)更复杂一些错误更容易。 - RodeoClown
如果它是一个不需要的简单删除值,并且循环只执行那一件事,直接使用迭代器并调用.remove()绝对没问题。 - RodeoClown


在这种情况下,一个常见的伎俩(是?)倒退:

for(int i = l.size() - 1; i >= 0; i --) {
  if (l.get(i) == 5) {
    l.remove(i);
  }
}

也就是说,我很高兴您在Java 8中有更好的方法,例如 removeIf 要么 filter 在溪流上。


17
2017-08-29 09:56



这是一个很好的技巧。但它不适用于像集合这样的非索引集合,并且在链表上说它真的很慢。 - Claudiu
@Claudiu是的,这绝对只是为了 ArrayLists或类似的集合。 - Landei
我正在使用ArrayList,这非常有效,谢谢。 - StarSweeper
索引很棒。如果这很常见,为什么不使用 for(int i = l.size(); i-->0;) {? - John


答案一样 克劳 使用for循环:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}

14
2017-08-21 12:39





Eclipse集合 (以前 GS系列), 方法 removeIf 定义 MutableCollection 将工作:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.lessThan(3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

使用Java 8 Lambda语法,可以按如下方式编写:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.cast(integer -> integer < 3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

打电话给 Predicates.cast() 这是必要的,因为默认 removeIf 方法被添加了 java.util.Collection Java 8中的接口。

注意: 我是一个提交者 Eclipse集合


11
2017-12-18 23:08





制作现有列表的副本并迭代新副本。

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}

6
2018-06-26 05:28



制作副本听起来像是浪费资源。 - Antzi
@Antzi这取决于列表的大小和内部对象的密度。仍然是有价值和有效的解决方案 - mre