如何在迭代ArrayList时删除元素而避免“ConcurrentModificationException”异常?

398

我正在尝试在迭代ArrayList时从中删除一些元素,代码如下:

for (String str : myArrayList) {
    if (someCondition) {
        myArrayList.remove(str);
    }
}

当我尝试在同时迭代myArrayList并从列表中删除项时,会出现ConcurrentModificationException。有没有简单的解决方法来解决这个问题?


1
我使用了克隆对象来解决这个问题。 - user2056463
10个回答

634

使用Iterator并调用remove()

Iterator<String> iter = myArrayList.iterator();

while (iter.hasNext()) {
    String str = iter.next();

    if (someCondition)
        iter.remove();
}

7
谢谢,现在一切正常 :) 我认为这个答案是最好的,因为代码很容易阅读。 - Ernestas Gruodis
6
权衡的是,iter现在在方法的其余部分都处于作用域范围内。 - Eric Stein
4
如果我想删除除当前迭代之外的其他内容(比如说它在索引2,但我需要同时删除索引7),该怎么办?每当我尝试通过.remove(index)进行操作时,它都会给我一个ConcurrentModificationException异常。 - user1433479
52
有趣,我在 String str = iter.next(); 上也遇到了相同的异常! Java 中的集合糟糕透了! - Al-Mothafar
3
使用这种方法时,我遇到了相同的异常。 - Aakash Patel
显示剩余7条评论

221

作为其他人答案的替代方案,我通常会做类似于这样的事情:

List<String> toRemove = new ArrayList<String>();
for (String str : myArrayList) {
    if (someCondition) {
        toRemove.add(str);
    }
}
myArrayList.removeAll(toRemove);

这样做可以避免你直接处理迭代器,但需要另一个列表。出于某些原因,我总是更喜欢这种方法。


28
+1 我喜欢这个无需迭代器的解决方案。 - Terry Li
5
使用比所需资源更多的原因是什么?迭代器并不难处理,也不会让代码变得混乱。 - Eric Stein
2
@EricStein 我通常会遇到想要添加到列表中的情况,而额外的资源大多是琐碎的。这只是一种替代方案,两者都有其优缺点。 - Kevin DiTraglia
@KevinDiTraglia 我同意资源通常是微不足道的。 - Eric Stein
4
如果我们采用不可变列表(例如 Guava 库中的列表),再进一步处理多线程并发问题时,这个方法会更加吸引人。 - Nobbynob Littlun
显示剩余5条评论

106

Java 8用户可以这样做:list.removeIf(...)

    List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
    list.removeIf(e -> (someCondition));
它将删除列表中满足一些条件的元素。

是的,如果您可以使用Java 8,那么这会更好。 - user377628
2
如果他们也添加了removeWhile,那就太好了。 - Konstantin Milyutin
@damluar,我不明白为什么removeWhileremoveIf会删除所有符合条件的元素。 - António Almeida
但是如果您只想在满足条件之前/之时删除第一个元素呢? - Konstantin Milyutin
他们在JDK 9中添加了类似于“removeWhile”的功能。 - Mikhail Boyarsky

70

你需要使用迭代器的remove()方法,这意味着不能使用增强型for循环:

for (final Iterator iterator = myArrayList.iterator(); iterator.hasNext(); ) {
    iterator.next();
    if (someCondition) {
        iterator.remove();
    }
}

10
我认为这个答案表达得更好;迭代器被限制在for循环中,并且迭代的细节在for语句中。减少视觉噪声。 - Nobbynob Littlun
如果您向迭代器添加类型参数,并将其分配给某个变量,以便在if语句中实际执行某些操作,则这是最佳解决方案。 - sscarduzio
为什么要将迭代器声明为final? - kh.tab
1
@kh.tab 我认为,将所有不打算重新分配的变量声明为final是一个好习惯。我只希望“final”是默认设置。 - Eric Stein

42

不,不,不!

在单线程任务中,您不需要使用Iterator,而且CopyOnWriteArrayList(由于性能问题)也不需要。

解决方案要简单得多:尝试使用传统的for循环而不是for-each循环

根据Java版权所有者(几年前是Sun,现在是Oracle)的for-each循环指南,它使用迭代器遍历集合,仅将其隐藏以使代码看起来更好。但是,不幸的是,正如我们所看到的那样,它产生了比收益更多的问题,否则就不会出现这个问题。

例如,当进入修改后的ArrayList的下一个迭代时,此代码将导致java.util.ConcurrentModificationException:

        // process collection
        for (SomeClass currElement: testList) {

            SomeClass founDuplicate = findDuplicates(currElement);
            if (founDuplicate != null) {
                uniqueTestList.add(founDuplicate);
                testList.remove(testList.indexOf(currElement));
            }
        }

但是以下代码可以正常工作:

    // process collection
    for (int i = 0; i < testList.size(); i++) {
        SomeClass currElement = testList.get(i);

        SomeClass founDuplicate = findDuplicates(currElement);
        if (founDuplicate != null) {
            uniqueTestList.add(founDuplicate);
            testList.remove(testList.indexOf(currElement));
            i--; //to avoid skipping of shifted element
        }
    }
因此,尝试使用索引方法来迭代集合并避免使用for-each循环,因为它们不相等! for-each循环使用一些内部迭代器,这些迭代器检查集合修改并抛出ConcurrentModificationException异常。要确认这一点,请仔细查看使用我发布的第一个示例时打印的堆栈跟踪:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
    at java.util.AbstractList$Itr.next(AbstractList.java:343)
    at TestFail.main(TestFail.java:43)

对于多线程,请使用相应的多任务方法(如 synchronized 关键字)。


19
值得注意的是,由于 LinkedList 内部工作方式的原因,使用 Iterator 迭代器比通过不断递增索引值调用 get(i) 方法要 高效得多 - Angad
非常好的评论和细节,谢谢。 - OlivierM
1
同意Angad的观点。通常我们只能访问到通用的List类型,而使用的具体实现则是未知的。如果使用的是LinkedList实现,那么使用C风格的for循环来遍历List并检索每个元素将导致O(n2)的复杂度。 - mdewit
7
您可以通过向下循环来避免“i--; //to avoid skipping of shifted element”的情况:for (int i = testList.size() - 1; i >= 0; i--) { ... }此外,您可以用testList.remove(i); 来替换testList.remove(testList.indexOf(currElement)); - Martin Rust
1
@Angad 但是使用迭代器会导致所提到的异常,因为它依赖于先前-当前-下一个关系,在从集合中删除元素的情况下这种关系被破坏。在这里我们应该承担性能损失。 - Dima Naychuk
@MartinRust 是的,谢谢您的评论! - Dima Naychuk

9

虽然其他建议的解决方案也可以工作,但如果你真的希望解决方案是线程安全的,你应该用CopyOnWriteArrayList代替ArrayList。

    //List<String> s = new ArrayList<>(); //Will throw exception
    List<String> s = new CopyOnWriteArrayList<>();
    s.add("B");
    Iterator<String> it = s.iterator();
    s.add("A");

    //Below removes only "B" from List
    while (it.hasNext()) {
        s.remove(it.next());
    }
    System.out.println(s);

2
是的,但Java文档说:“这通常代价太高,但当遍历操作比变异操作多得多时可能更有效,并且在您无法或不想同步遍历操作但需要防止并发线程之间的干扰时非常有用。” - Ernestas Gruodis

8

如果您希望在遍历期间修改列表,则需要使用Iterator。然后,您可以使用iterator.remove()来删除遍历过程中的元素。


7

一种替代方法是将您的 List 转换为 array,对它们进行迭代,并根据您的逻辑直接从 List 中删除它们。

List<String> myList = new ArrayList<String>(); // You can use either list or set

myList.add("abc");
myList.add("abcd");
myList.add("abcde");
myList.add("abcdef");
myList.add("abcdefg");

Object[] obj = myList.toArray();

for(Object o:obj)  {
    if(condition)
        myList.remove(o.toString());
}

1
为什么在删除时会有object.toString()?难道不应该只是'o'吗? - themorfeus
@TheMorfeus 可以只用'o'。但我使用toString()方法是为了避免IDE中的“可疑方法调用”错误。没有其他特定的原因。 - CarlJohn
2
这个解决方案仅适用于列表大小较小的情况。想象一下,如果列表包含数千个项目,转换为数组将非常昂贵。 - Shailesh Saxena

7
List myArrayList  = Collections.synchronizedList(new ArrayList());

//add your elements  
 myArrayList.add();
 myArrayList.add();
 myArrayList.add();

synchronized(myArrayList) {
    Iterator i = myArrayList.iterator(); 
     while (i.hasNext()){
         Object  object = i.next();
     }
 }

2
在这个答案中,您从列表中删除了哪些项目? OP问如何避免在删除元素时出现“ConcurrentModificationException”。我看不出其他人为什么会赞同这个答案。 - Shailesh Saxena

2
您可以使用迭代器的 remove() 函数从底层集合对象中删除对象。但在这种情况下,您只能从列表中删除相同的对象,而不能删除其他对象。
来自这里

1
链接应该放在评论区,除非它们支持您的帖子。您应该编辑您的答案以包括解释,然后只将链接作为参考。 - CodeCamper
这个确实有效,解决了问题!谢谢! - Vasilije Bursac

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