如何避免在迭代ArrayList并删除元素时出现java.util.ConcurrentModificationException异常

292

我有一个ArrayList需要遍历。在遍历时,我需要同时删除元素。显然这会抛出一个java.util.ConcurrentModificationException异常。

如何处理这个问题的最佳实践是什么?我应该先克隆列表吗?

我不是在循环本身中删除元素,而是在代码的另一部分中删除元素。

我的代码如下:

public class Test() {
    private ArrayList<A> abc = new ArrayList<A>();

    public void doStuff() {
        for (A a : abc) 
        a.doSomething();
    }

    public void removeA(A a) {
        abc.remove(a);
    }
}

a.doSomething可能会调用Test.removeA();


http://www.javacodegeeks.com/2011/05/avoid-concurrentmodificationexception.html - bilash.saha
25个回答

429

有两个选项:

  • 创建一个你希望删除的值的列表,在循环中添加到该列表中,然后在末尾调用originalList.removeAll(valuesToRemove)
  • 直接在迭代器上使用remove()方法。请注意,这意味着您不能使用增强型for循环。

作为第二种选项的示例,从列表中删除长度大于5的任何字符串:

List<String> list = new ArrayList<String>();
...
for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
    String value = iterator.next();
    if (value.length() > 5) {
        iterator.remove();
    }
}

3
我应该提到,我将元素删除在代码的另一部分而不是循环本身。 - RoflcoptrException
2
这个解决方案也会导致java.util.ConcurrentModificationException异常,请参见https://dev59.com/SGMl5IYBdhLWcg3wZGLo#18448699。 - CoolMind
@CoolMind:嗯,不是的。你链接的答案和我的等价 - 你认为有什么区别? - Jon Skeet
@JonSkeet,你是对的,但在我的情况下它引发了异常,但另一种变体起作用了。抱歉。 - CoolMind
1
@CoolMind:如果没有多线程,这段代码应该没问题。 - Jon Skeet
显示剩余13条评论

31

来自ArrayList的JavaDocs

该类的iterator和listIterator方法返回的迭代器是快速失败的:如果在迭代器创建后的任何时间结构上修改列表,除了通过迭代器自己的remove或add方法之外的任何方式,迭代器将抛出ConcurrentModificationException。


31
问题的答案在哪里? - Adelin
6
就像它所说的那样,“除非通过迭代器自己的remove或add方法”。 - Varun Achar

29
尝试在高级“for循环”中从列表中删除值是不可能的,即使您在代码中应用任何技巧(就像您在代码中所做的那样)。 更好的方法是按照其他建议将代码迭代器级别编写。 我想知道为什么人们没有建议传统的for循环方法。
for( int i = 0; i < lStringList.size(); i++ )
{
    String lValue = lStringList.get( i );
    if(lValue.equals("_Not_Required"))
    {
         lStringList.remove(lValue);
         i--; 
    }  
}

这同样有效。


5
这是不正确的!!! 当您删除一个元素时,下一个元素会占据它的位置,而当 i 增加时,下一个元素在下一次迭代中不会被检查。 在这种情况下,您应该使用( int i = lStringList.size(); i>-1; i-- )。 - Johntor
2
同意!另一种方法是在for循环中的if条件语句中执行i--;。 - suhas0sn07
1
我认为这个答案已经被编辑以解决上面评论中的问题,所以现在它对我来说运行良好。 - Kira Resari
@KiraResari,是的。我已经更新了答案来解决这个问题。 - suhas0sn07
谢谢。你的回答帮助我解决了我的问题 :) - undefined

18

在Java 8中,您可以使用Collection接口并通过调用removeIf方法来实现此操作:

yourList.removeIf((A a) -> a.value == 2);

更多信息可以在这里找到


1
在Android中,Call需要API级别24(Android Nougat) - Ahmed Maad

12

你真的应该按照传统方式迭代数组。

每次从列表中删除一个元素后,后面的元素会向前移动。只要不改变除正在迭代的元素以外的元素,以下代码应该可以工作。

public class Test(){
    private ArrayList<A> abc = new ArrayList<A>();

    public void doStuff(){
        for(int i = (abc.size() - 1); i >= 0; i--) 
            abc.get(i).doSomething();
    }

    public void removeA(A a){
        abc.remove(a);
    }
}

8

在遍历列表时,如果您想要删除元素是可能的。让我们看下面的例子,

ArrayList<String>  names = new ArrayList<String>();
        names.add("abc");
        names.add("def");
        names.add("ghi");
        names.add("xyz");

我有一个数组列表,其中包含上述名称。我想从列表中删除“def”名称。

for(String name : names){
    if(name.equals("def")){
        names.remove("def");
    }
}

以上代码会抛出ConcurrentModificationException异常,因为您在迭代时修改了列表。

因此,要按照以下方式从Arraylist中删除“def”名称:

Iterator<String> itr = names.iterator();            
while(itr.hasNext()){
    String name = itr.next();
    if(name.equals("def")){
        itr.remove();
    }
}

通过迭代器,我们可以从Arraylist中删除“def”名称并尝试打印数组,您将看到以下输出。
输出:[abc,ghi,xyz]

否则,我们可以使用并发包中提供的并发列表,以便您可以在迭代时执行删除和添加操作。例如,请参见下面的代码片段。ArrayList<String> names = new ArrayList<String>(); CopyOnWriteArrayList<String> copyNames = new CopyOnWriteArrayList<String>(names); for(String name : copyNames){ if(name.equals("def")){ copyNames.remove("def"); } } - Indra K
CopyOnWriteArrayList 将是最昂贵的操作。 - Indra K

5

按照正常的方式进行循环,java.util.ConcurrentModificationException 是与被访问元素相关的错误。

因此,请尝试以下方法:

for(int i = 0; i < list.size(); i++){
    lista.get(i).action();
}

1
你通过不从列表中删除任何东西来避免了java.util.ConcurrentModificationException。有点棘手 :) 你真的不能称这为迭代列表的“正常方式”。 - Zsolt Sky

5

这里有一个例子,我使用了另一个列表来添加要删除的对象,然后使用 stream.foreach 从原始列表中删除元素:

private ObservableList<CustomerTableEntry> customersTableViewItems = FXCollections.observableArrayList();
...
private void removeOutdatedRowsElementsFromCustomerView()
{
    ObjectProperty<TimeStamp> currentTimestamp = new SimpleObjectProperty<>(TimeStamp.getCurrentTime());
    long diff;
    long diffSeconds;
    List<Object> objectsToRemove = new ArrayList<>();
    for(CustomerTableEntry item: customersTableViewItems) {
        diff = currentTimestamp.getValue().getTime() - item.timestamp.getValue().getTime();
        diffSeconds = diff / 1000 % 60;
        if(diffSeconds > 10) {
            // Element has been idle for too long, meaning no communication, hence remove it
            System.out.printf("- Idle element [%s] - will be removed\n", item.getUserName());
            objectsToRemove.add(item);
        }
    }
    objectsToRemove.stream().forEach(o -> customersTableViewItems.remove(o));
}

我认为你在执行两个循环时做了额外的工作,最坏的情况下,这些循环将遍历整个列表。只使用一个循环会更简单、更经济。 - Luis Carlos
我认为你不能在第一个循环中删除对象,因此需要额外的删除循环,而且删除循环仅用于删除对象 - 也许你可以写一个只有一个循环的示例,我想看看 - 谢谢@LuisCarlos - serup
正如你所说,使用这段代码无法在for循环内部删除任何元素,因为它会导致java.util.ConcurrentModificationException异常。但是你可以使用基本的for循环。这里我写了一个例子,使用了你的代码的一部分。 - Luis Carlos
1
for(int i = 0; i < customersTableViewItems.size(); i++) { diff = currentTimestamp.getValue().getTime() - customersTableViewItems.get(i).timestamp.getValue().getTime(); diffSeconds = diff / 1000 % 60; if(diffSeconds > 10) { customersTableViewItems.remove(i--); } }i--很重要,因为你不想跳过任何元素。 此外,您可以使用ArrayList类提供的removeIf(Predicate <? super E> filter)方法。 希望这有所帮助。 - Luis Carlos
@LuisCarlos,如果这是可能的,为什么使用另一种方式时会出现异常?也许这不安全,无论如何感谢您的示例。 - serup
1
异常发生是因为在for循环中存在对列表迭代器的活动引用。在普通的for循环中,没有引用,您可以更灵活地更改数据。 希望这可以帮助到您。 - Luis Carlos

5

您也可以使用CopyOnWriteArrayList而不是ArrayList。这是从JDK 1.5开始推荐的最新方法。


4
一种选项是将removeA方法修改为以下内容-
public void removeA(A a,Iterator<A> iterator) {
     iterator.remove(a);
     }

但这意味着您的 doSomething() 应该能够将 iterator 传递给 remove 方法。这不是一个很好的主意。
您能否采用两个步骤的方法来解决:
在第一个循环中遍历列表时,不要删除选定的元素,而是将它们标记为要删除的元素。为此,您可以将这些元素(浅复制)简单地复制到另一个 List 中。
然后,一旦迭代完成,只需从第一个列表中使用 removeAll 删除第二个列表中的所有元素即可。

很好,我使用了相同的方法,尽管我循环了两次。这使得事情变得简单,并且没有并发问题 :) - Pankaj Nimgade
2
我没有看到Iterator有一个remove(a)方法。remove()不带参数 https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html 我错过了什么? - c0der
1
@c0der是正确的。我的意思是这个怎么会被投了5次票... - Kasasira

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