在循环集合时避免ConcurrentModificationException异常,删除对象的迭代

1297

我们都知道由于 ConcurrentModificationException,您不能执行以下操作:

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

但这似乎有时有效,但并非总是如此。以下是一些特定代码:

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

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

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

    System.out.println(l);
}

当然,这会导致:

Exception in thread "main" java.util.ConcurrentModificationException

虽然有多个线程,但这些线程并没有处理它。不管怎样。

如何解决此问题?如何在循环中从集合中删除一个项而不抛出异常?

我在这里还使用了一个任意的Collection,不一定是ArrayList,因此不能依赖于get方法。


1
读者注意:请阅读http://docs.oracle.com/javase/tutorial/collections/interfaces/collection.html,它可能有更简单的方法来实现您想要做的事情。 - GKFX
31个回答

1672

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相同-它被设计为允许这样做。


在您的情况下,您尝试从列表中删除,但是如果尝试在迭代其内容时putMap中,则同样适用此限制。


21
如果你想删除当前迭代返回的元素之外的其他元素,该怎么办? - Eugen
2
你必须在迭代器中使用 .remove 方法,该方法只能删除当前元素,所以不 :) - Bill K
1
请注意,与使用ConcurrentLinkedDeque或CopyOnWriteArrayList相比(至少在我的情况下),这种方法速度较慢。 - Dan
1
在for循环中放置iterator.next()调用不可能吗?如果不行,有人能解释一下为什么吗? - Blake
1
从Java 8开始,这种方法已经内置为l.removeIf(i -> condition(i));,参见https://dev59.com/z3I-5IYBdhLWcg3wYnOQ#29187813。 - JJ Brown
显示剩余13条评论

359

这个有效:

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

我曾经认为foreach循环只是语法糖,使用迭代器没有帮助...但它提供了这个.remove()功能。


48
foreach循环本质上是迭代的语法糖。但正如你指出的,你需要在迭代器上调用remove方法,而foreach并没有给你访问迭代器的权限。这就是为什么你不能在foreach循环中删除元素的原因(尽管实际上在幕后使用的确实是迭代器)。 - madlep
36
+1 是为了举例说明如何在上下文中使用 iter.remove(),而 Bill K 的回答并没有[直接]提到这一点。 - Eddified

242
在Java 8中,你可以使用新的removeIf方法。应用到你的示例中:
Collection<Integer> coll = new ArrayList<>();
//populate

coll.removeIf(i -> i == 5);
一个简单的测试示例:
    @Test
    public void testRemoveIfOneList() {
        List<String> outer = new ArrayList<>();
        outer.add("one");
        outer.add("two");
        outer.add("three");

        outer.removeIf(o -> o.length() == 3);

        assertEquals(1, outer.size());
    }

即使您要比较两个列表并希望从两个列表中删除,它也可以正常工作。

    @Test
    public void testRemoveIfTwoLists() {
        List<String> outer = new ArrayList<>();
        outer.add("one");
        outer.add("two");
        outer.add("three");
        List<String> inner = new ArrayList<>();
        inner.addAll(outer);

        // first, it removes from inner, and if anything is removed, then removeIf() returns true,
        // leading to removing from outer
        outer.removeIf(o -> inner.removeIf(i -> i.equals(o)));

        assertEquals(0, outer.size());
        assertEquals(0, inner.size());
    }

然而,如果列表中有重复项,请确保在内部循环中迭代它,因为对于内部列表,它将删除符合条件的所有元素,但对于外部列表,当删除任何一个元素时,它将立即返回并停止检查。

这个测试将失败:

    @Test
    public void testRemoveIfTwoListsInnerHasDuplicates() {
        List<String> outer = new ArrayList<>();
        outer.add("one");
        outer.add("one");
        outer.add("two");
        outer.add("two");
        outer.add("three");
        outer.add("three");
        List<String> inner = new ArrayList<>();
        inner.addAll(outer); // both have duplicates

        // remove all elements from inner(executed twice), then remove from outer
        // but only once! if anything is removed, it will return immediately!!
        outer.removeIf(o -> inner.removeIf(i -> i.equals(o)));

        assertEquals(0, inner.size()); // pass, inner all removed
        assertEquals(0, outer.size()); // will fail, outer has size = 3
    }

4
哦哦哦!我希望Java 8或9中的某些东西可以帮助。对我来说,这仍然似乎有点啰嗦,但我仍然喜欢它。 - James T Snell
在这种情况下,实现equals()方法是否被推荐? - Anmol Gupta
1
顺便提一下,removeIf 使用 Iteratorwhile 循环。你可以在 Java 8 的 java.util.Collection.java 中看到它。 - omerhakanbilici
3
有些实现(如ArrayList)出于性能原因会对其进行重写。而你提到的那个只是默认实现。 - Didier L
@AnmolGupta:不,这里根本没用到 equals,所以不必实现它。(但是当然,如果你在测试中使用了 equals,那么就必须按照你想要的方式来实现它。) - Lii
现在这应该被接受为答案,这将适用并受到大多数人寻找答案的欢迎。 - Anupam

44

既然问题已经得到了答案,即最好的方法是使用迭代器对象的remove方法,那么我将进一步说明出现错误“java.util.ConcurrentModificationException”的地方的具体细节。

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

next()方法的代码大致如下...

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

因此,正如您所看到的,如果您明确尝试从集合中删除一个元素。 这将导致modCountexpectedModCount不同,从而导致异常ConcurrentModificationException


1
非常有趣。谢谢!我经常不主动调用remove(),而是更喜欢在迭代完成后清除集合。并不是说这是一个好的模式,只是最近我一直这么做。 - James T Snell

28

你可以直接使用迭代器,就像你所提到的那样,或者保留第二个集合并将要删除的每个项添加到新集合中,然后在最后进行 removeAll 操作。这样可以在保持 for-each 循环的类型安全性的同时增加内存使用和 CPU 时间(除非你有非常大的列表或非常旧的计算机,否则不应该成为一个很大的问题)。

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

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

7
这通常是我的做法,但我觉得显式迭代器更加优雅。 - Claudiu
1
好的,只要你不对迭代器做其他操作就可以了 - 将其暴露出来会使调用.next()两次的循环更容易等等。 这不是一个大问题,但如果你要做的事情比仅仅遍历列表删除条目还要复杂,可能会引发一些问题。 - RodeoClown
@RodeoClown:在原问题中,Claudiu是从集合(Collection)中移除,而不是迭代器(iterator)。 - matt b
1
从迭代器中删除会从基础集合中删除...但是我在上一条评论中所说的是,如果你要做的事情比在循环中查找删除更复杂(比如处理正确的数据),使用迭代器可能会使某些错误更容易发生。 - RodeoClown
如果只是简单地删除不需要的值,并且循环只执行这一项任务,直接使用迭代器并调用 .remove() 是完全可以的。 - RodeoClown
使用 Iterator.remove() 只能作用于迭代器提供的当前值。如果您的循环中有某些条件需要删除集合中的其他成员,则 RodeoClown 的解决方案是可行的。 - PMorganCA

19
在这种情况下,一个常见的技巧是(曾经是?)向后倒退:
for(int i = l.size() - 1; i >= 0; i --) {
  if (l.get(i) == 5) {
    l.remove(i);
  }
}

话虽如此,我很高兴你在Java 8中有更好的方法,例如对流使用removeIffilter


2
这是一个不错的技巧。但是它在非索引集合(如集合)上无法工作,并且在链表等数据结构上速度会非常慢。 - Claudiu
@Claudiu 是的,这绝对只适用于ArrayList或类似集合。 - Landei
我正在使用ArrayList,这个很完美,谢谢。 - StarSweeper
2
索引非常好用。既然它这么常见,为什么不使用for(int i = l.size(); i-->0;) {呢? - John

17

使用for循环的相同答案,参考Claudius

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

12

使用Eclipse Collections,在MutableCollection上定义的removeIf方法将起作用:

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()是必要的,因为Java 8中的java.util.Collection接口添加了默认的removeIf方法。

注意:我是Eclipse Collections的提交者。


11

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

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

20
复制听起来像是浪费资源。 - Antzi
4
这取决于列表的大小和其中物体的密度。仍然是一种有价值且有效的解决方案。 - mre
我一直在使用这种方法。它需要更多的资源,但更加灵活和清晰。 - Tao Zhang
这是一个很好的解决方案,当你不打算在循环内部删除对象,但它们却被其他线程“随机”删除(例如网络操作更新数据)时。如果你发现自己经常需要进行这些复制操作,甚至有一个Java实现可以完全做到这一点:https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CopyOnWriteArrayList.html - A1m
复制列表通常是在Android上使用监听器时所做的。这对于小型列表是一个有效的解决方案。 - Slion

10

使用传统的for循环

ArrayList<String> myArray = new ArrayList<>();

for (int i = 0; i < myArray.size(); ) {
    String text = myArray.get(i);
    if (someCondition(text))
        myArray.remove(i);
    else
        i++;   
}

1
啊,所以真正抛出异常的只是增强型for循环。 - cellepo
同样的代码,如果在循环保护中修改为递增“i ++”,而不是在循环体内递增,仍然可以正常工作。 - cellepo
更正 ^:那就是如果 i++ 自增不受条件限制 - 我现在明白了,这就是为什么你要在循环体中这样做 :) - cellepo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接