遍历Collection,避免在循环中删除对象时避免ConcurrentModificationException


1194

我们都知道由于以下原因您无法执行以下操作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这里使用任意值,不一定是an ArrayList,因此您不能依赖get


读者注意:请确实阅读docs.oracle.com/javase/tutorial/collections/interfaces/…,它可能更容易实现您想做的事情。
GKFX'5

Answers:


1601

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 -旨在允许它。


你的情况,你想从列表中删除,但同样的限制,如果想put成为一个Map在迭代的内容。


19
如果要删除当前迭代中返回的元素以外的其他元素怎么办?
Eugen

2
您必须在迭代器中使用.remove,并且它只能删除当前元素,所以没有:)
Bill K

1
请注意,与使用ConcurrentLinkedDeque或CopyOnWriteArrayList相比,这种方法要慢(至少在我的情况下)
Dan

1
无法将iterator.next()调用置于for循环中吗?如果没有,有人可以解释为什么吗?
布莱克2016年

1
@GonenI它是为集合中所有不变的迭代器实现的。List.add在同样的意义上也是“可选”的,但是您不会说添加到列表中是“不安全的”。
Radiodef

345

这有效:

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

我假设因为foreach循环是用于迭代的语法糖,所以使用迭代器无济于事...但是它为您提供了此.remove()功能。


43
foreach循环用于迭代的语法糖。但是,正如您所指出的那样,您需要在迭代器上调用remove-foreach无法授予您访问权限。因此,你为什么不能在foreach循环(即使你删除的原因实际使用引擎盖下一个迭代器)
madlep

36
+1例如在上下文中使用iter.remove()的代码,Bill K的答案没有[直接]。
2012年

202

在Java 8中,可以使用removeIf方法。应用于您的示例:

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

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

3
!!我希望Java 8或9可以有所帮助。这对我来说似乎仍然很冗长,但是我仍然喜欢它。
James T Snell 2015年

在这种情况下是否也建议实现equals()?
Anmol Gupta 2015年

顺便removeIf使用Iteratorwhile循环。您可以在Java 8java.util.Collection.java
omerhakanbilici

3
@omerhakanbilici ArrayList出于性能原因,有些实现将其覆盖。您所指的只是默认实现。
Didier L

@AnmolGupta:不,equals在这里完全不使用,因此不必实现。(但是,当然,如果要equals在测试中使用,则必须按照所需的方式实施。)
Lii

42

由于问题已经得到解决,即最好的方法是使用迭代器对象的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


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

26

您可以像上面提到的那样直接使用迭代器,也可以保留另一个集合,然后将要删除的每个项目添加到新集合中,然后在最后删除removeAll。这使您能够以增加内存使用量和cpu时间为代价继续使用for-each循环的类型安全性(除非您拥有非常大的列表或非常老的计算机,否则这不是一个大问题)

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从集合中删除,而不是迭代器。
马特b

1
从迭代器中删除会从基础集合中删除...但是我在最后一条评论中说的是,如果您做的事情比仅使用迭代器在循环中查找删除(例如处理正确的数据)还要复杂,那么错误更容易造成。
RodeoClown

如果这是一个简单的删除值,并且不需要,而循环仅执行该操作,则直接使用迭代器并调用.remove()是绝对可以的。
RodeoClown

17

在这种情况下,通常的诀窍是(过去是)倒退:

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

尽管如此,我很乐意,你必须在Java中8,如更好的方式removeIffilter在流。


2
这是一个好把戏。但是它不适用于集合等非索引集合,并且在说链表时确实很慢。
2014年

@Claudiu是的,这绝对仅适用于ArrayLists或类似的集合。
兰代2014年

我正在使用ArrayList,这工作得很好,谢谢。
StarSweeper '18

2
索引很棒。如果很常见,为什么不使用for(int i = l.size(); i-->0;) {
约翰(John)

16

Claudius的答案相同,带有for循环:

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 removeIfjava.util.Collection接口上添加了默认方法。

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


10

制作现有列表的副本,并遍历新副本。

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

19
进行复制听起来像是在浪费资源。
Antzi

3
@Antzi这取决于列表的大小和其中对象的密度。仍然是有价值且有效的解决方案。
mre

我一直在使用这种方法。它需要更多的资源,但是更加灵活和清晰。
张涛

当您不打算删除循环本身内部的对象,而是从其他线程中“随机”删除它们时(例如,网络操作更新数据),这是一个很好的解决方案。如果您发现自己经常
A1m

8

人们断言不能从被foreach循环迭代的Collection中删除。我只是想指出这在技术上是不正确的,并准确地描述了该假设背后的代码(我知道OP的问题非常先进,以至于不知道这一点)。

for (TouchableObj obj : untouchedSet) {  // <--- This is where ConcurrentModificationException strikes
    if (obj.isTouched()) {
        untouchedSet.remove(obj);
        touchedSt.add(obj);
        break;  // this is key to avoiding returning to the foreach
    }
}

不是您不能从迭代中删除,Colletion而是一旦完成就无法继续迭代。因此,break在上面的代码中。

抱歉,如果此答案是一个有点专业的用例,并且更适合于我从此处到达的原始线程,那该标记为该线程的副本(尽管此线程看上去更细微)并被锁定。


8

与传统的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++;   
}

嗯,所以它实际上只是引发异常的增强的 -for循环。
cellepo

FWIW-修改为i++在循环保护中而不是在循环主体中递增后,相同的代码仍然可以工作。
cellepo

更正^:那就是如果i++增量不是有条件的-我现在知道这就是为什么要在体内进行:)
cellepo

2

A ListIterator允许您添加或删除列表中的项目。假设您有一个Car对象列表:

List<Car> cars = ArrayList<>();
// add cars here...

for (ListIterator<Car> carIterator = cars.listIterator();  carIterator.hasNext(); )
{
   if (<some-condition>)
   { 
      carIterator().remove()
   }
   else if (<some-other-condition>)
   { 
      carIterator().add(aNewCar);
   }
}

ListIterator接口中的其他方法(Iterator的扩展)很有趣-尤其是它的previous方法。
cellepo

1

我对以上问题有一个建议。无需二级列表或任何额外的时间。请找到一个示例,该示例将执行相同的操作,但使用不同的方式。

//"list" is ArrayList<Object>
//"state" is some boolean variable, which when set to true, Object will be removed from the list
int index = 0;
while(index < list.size()) {
    Object r = list.get(index);
    if( state ) {
        list.remove(index);
        index = 0;
        continue;
    }
    index += 1;
}

这样可以避免并发异常。


1
该问题明确指出,OP不必使用ArrayList,因此不能依赖get()。否则,可能是一个好方法。
kaskelotti 2014年

(澄清^)OP是使用任意的Collection- Collection界面不包括get。(尽管FWIW List界面确实包含“ get”)。
cellepo

我刚刚在此处还添加了一个单独的,更详细的答案,还用于while-a循环List。但此答案+1,因为它排在第一位。
cellepo

1

ConcurrentHashMapConcurrentLinkedQueueConcurrentSkipListMap可能是另一个选择,因为即使您删除或添加项目,它们也不会抛出任何ConcurrentModificationException。


是的,请注意,这些都在java.util.concurrent包装中。该软件包中的其他一些类似/通用案例类是CopyOnWriteArrayList CopyOnWriteArraySet [但不限于那些]。
cellepo

实际上,我刚刚了解到,尽管这些数据结构对象避免了 ConcurrentModificationException使用,但在增强的IndexOutOfBoundsException
for

1

我知道这个问题对于Java 8来说已经太老了,但是对于那些使用Java 8的人来说,您可以轻松地使用removeIf():

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

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

1

另一种方法是创建arrayList的副本:

List<Object> l = ...

List<Object> iterationList = ImmutableList.copyOf(l);

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

注意:i不是index,而是对象。也许称它obj会更合适。
luckydonald

1

最好的方法(推荐)是使用java.util.Concurrent包。通过使用此包,您可以轻松避免此Exception。参考修改后的代码

public static void main(String[] args) {
    Collection<Integer> l = new CopyOnWriteArrayList<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);
}

0

ArrayList:remove(int index)-if(index是最后一个元素的位置)的情况下System.arraycopy(),它避免了这种情况,并且不需要花费时间。

arraycopy时间增加if(index减少),通过list元素也减少!

最好的有效删除方法是-降序删除其元素: while(list.size()>0)list.remove(list.size()-1);//取O(1) while(list.size()>0)list.remove(0);//取O(factorial(n))

//region prepare data
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<Integer> toRemove = new ArrayList<Integer>();
Random rdm = new Random();
long millis;
for (int i = 0; i < 100000; i++) {
    Integer integer = rdm.nextInt();
    ints.add(integer);
}
ArrayList<Integer> intsForIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsDescIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsIterator = new ArrayList<Integer>(ints);
//endregion

// region for index
millis = System.currentTimeMillis();
for (int i = 0; i < intsForIndex.size(); i++) 
   if (intsForIndex.get(i) % 2 == 0) intsForIndex.remove(i--);
System.out.println(System.currentTimeMillis() - millis);
// endregion

// region for index desc
millis = System.currentTimeMillis();
for (int i = intsDescIndex.size() - 1; i >= 0; i--) 
   if (intsDescIndex.get(i) % 2 == 0) intsDescIndex.remove(i);
System.out.println(System.currentTimeMillis() - millis);
//endregion

// region iterator
millis = System.currentTimeMillis();
for (Iterator<Integer> iterator = intsIterator.iterator(); iterator.hasNext(); )
    if (iterator.next() % 2 == 0) iterator.remove();
System.out.println(System.currentTimeMillis() - millis);
//endregion
  • 用于索引循环:1090毫秒
  • 对于desc索引:519毫秒---最佳
  • 对于迭代器:1043毫秒

0
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

如果您跳过内部iterator.next()调用,则从列表中删除该元素之后便是捕获。它仍然有效!尽管我不建议编写这样的代码,但有助于理解其背后的概念:-)

干杯!


0

线程安全集合修改的示例:

public class Example {
    private final List<String> queue = Collections.synchronizedList(new ArrayList<String>());

    public void removeFromQueue() {
        synchronized (queue) {
            Iterator<String> iterator = queue.iterator();
            String string = iterator.next();
            if (string.isEmpty()) {
                iterator.remove();
            }
        }
    }
}

0

我知道这个问题只是假设一个Collection,而不是假设任何一个List。但是对于那些确实在使用List引用的阅读此问题的人,可以避免ConcurrentModificationException使用while-loop(在其中进行修改),而不要避免Iterator(如果您想总体上避免它,或者专门为了实现而避免使用它)一个不同于在每个元素上从头到尾停止的循环顺序(我相信这是唯一Iterator可以执行的顺序)):

*更新:请参见下面的注释,以澄清类似情况也可以通过传统的 -for循环实现。

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 1;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i++);

    } else {
        i += 2;
    }
}

该代码没有ConcurrentModificationException。

在这里,我们看到循环不是从头开始,也不是在每个元素上停止(我相信Iterator它本身无法做到)。

FWIW我们也看到get被调用了list,如果它的引用只是Collection(而不是更具体的List类型Collection)-interface Listincludes get,但是Collectioninterface并不是,则无法完成。如果不是因为这种差异,则list参考可以改为Collection[因此,从技术上讲,此答案将是直接答案,而不是切向答案]。

FWIWW相同的代码经过修改后可以在每个元素的停止处开始(仍然像Iterator顺序)一样仍然有效:

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 0;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i);

    } else {
        ++i;
    }
}

但是,这仍然需要非常仔细地计算要删除的指标。
OneCricketeer

此外,这只是该答案的更详细说明stackoverflow.com/a/43441822/2308683
OneCricketeer

必知-谢谢!其他的回答让我了解到,这是增强的 -for循环会抛出ConcurrentModificationException,但不是传统的 -for循环(这对方的回答用途) -却没有意识到之前,所以我的动机是写这个答案(我错误地当时以为是所有 for循环都会引发异常)。
cellepo

0

一种解决方案是旋转列表并删除第一个元素,以避免ConcurrentModificationException或IndexOutOfBoundsException

int n = list.size();
for(int j=0;j<n;j++){
    //you can also put a condition before remove
    list.remove(0);
    Collections.rotate(list, 1);
}
Collections.rotate(list, -1);

0

试试这个(删除列表中等于的所有元素i):

for (Object i : l) {
    if (condition(i)) {
        l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
    }
}

-2

这可能不是最好的方法,但是对于大多数小情况,这应该可以接受:

“创建第二个空数组,并仅添加要保留的空数组”

我不记得我从哪里读来的……为了公正起见,我将制作此Wiki,以希望有人找到它或只是不赚我不配的代表。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.