关于Java:迭代集合,在循环中删除对象时避免使用ConcurrentModificationException

Iterating through a Collection, avoiding ConcurrentModificationException when removing objects in a loop

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

1
2
3
4
5
for (Object i : l) {
    if (condition(i)) {
        l.remove(i);
    }
}

ConcurrentModificationException等…这显然有时有效,但并非总是如此。下面是一些特定的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}

当然,这会导致:

1
Exception in thread"main" java.util.ConcurrentModificationException

…即使多个线程没有这样做…不管怎样。

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
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,则同样的限制也适用。


这工作:

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

我假设既然foreach循环是迭代的语法甜头,使用迭代器没有帮助…但它提供了这个.remove()功能。


使用Java 8,您可以使用新的EDCOX1×16的方法。应用于您的示例:

1
2
3
4
Collection<Integer> coll = new ArrayList<>();
//populate

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


由于问题已经得到了回答,即最好的方法是使用迭代器对象的remove方法,因此我将详细介绍抛出错误"java.util.ConcurrentModificationException"的位置。

每个集合类都有一个私有类,它实现迭代器接口,并提供诸如next()remove()hasNext()等方法。

下一步的代码如下所示…

1
2
3
4
5
6
7
8
9
10
11
public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

在这里,方法checkForComodification实现为

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

因此,如您所见,如果您显式地尝试从集合中移除元素。这导致modCountexpectedModCount有所不同,导致ConcurrentModificationException例外。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}


在这种情况下,一个常见的诀窍是(曾经?)向后走:

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

也就是说,我很高兴在Java 8中有更好的方法,例如EDCOX1,16,或EDOCX1,22在流上。


与Claudius回答相同,带有for循环:

1
2
3
4
5
6
for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}

对于Eclipse集合(以前称为GS集合),在可变集合上定义的方法removeIf将起作用:

1
2
3
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语法,可以写如下:

1
2
3
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);

这里需要调用EDCOX1 17,因为在Java 8的EDCOX1 19接口中添加了一个默认的EDCOX1×16的方法。

注意:我是Eclipse集合的提交者。


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

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


人们断言foreach循环正在迭代的集合中不能移除。我只是想指出这在技术上是不正确的,并准确地描述(我知道操作的问题是如此高级,以避免知道这一点)该假设背后的代码:

1
2
3
4
5
6
7
    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

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


带有传统for循环

1
2
3
4
5
6
7
8
9
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++;
      }


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
   }
}


我对上述问题有个建议。不需要第二个列表或任何额外的时间。请找一个例子来做同样的事情,但方式不同。

1
2
3
4
5
6
7
8
9
10
11
12
//"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;
}

这将避免并发异常。


ConcurrentHashMap、ConcurrentLinkedQueue或ConcurrentSkipListMap可能是另一个选项,因为它们不会抛出任何ConcurrentModificationException,即使您删除或添加了项。


The Best way(Recommended) is use of java.util.Concurrent package . By
using this package you can easily avoid this Exception . refer
Modified Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
    }

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

1
2
3
4
5
6
7
8
List<Object> l = ...

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

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

}


1
2
3
4
5
6
7
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

如果跳过internal iterator.next()调用,则catch是从列表中删除元素后的。它仍然有效!虽然我不打算写这样的代码,但它有助于理解它背后的概念:—)

干杯!


线程安全集合修改示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
            }
        }
    }
}

我知道这个问题对于Java 8来说太老了,但是对于那些使用Java 8的人来说,你可以很容易地使用ReaveIf():

1
2
3
4
5
6
7
8
9
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);


我知道这个问题假设的只是一个Collection,而不是更具体地说,任何List。但是对于那些阅读这个问题的人来说,如果你想避免Iterator,你可以避免while循环的ConcurrentModificationException,而不是避免Iterator(一般来说,如果你想避免它,或者特别避免它,以实现不同于从头到尾的循环顺序。g在每个元素上(我认为这是Iterator本身所能做的唯一命令]):

*更新:请参阅下面的注释,说明传统for循环也可以实现类似的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
    }
}

除了该代码之外,没有ConcurrentModification。

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

fwiw我们还看到get被调用在List上,如果它的引用只是Collection类型(而不是更具体的List类型的Collection类型),则无法实现(List接口包括get类型,但Collection接口不包括。如果不是因为这个差异,那么List参考可以是Collection[因此从技术上讲,这个答案将是一个直接的答案,而不是一个切向的答案]。

fwiww相同的代码在修改后仍然有效,从每个元素的停止处开始(就像Iterator顺序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
    }
}


如果arraylist:remove(int index)-如果(index是最后一个元素的位置),则在没有System.arraycopy()的情况下,它会避免,并且不会花时间。

如果(索引减少),则arraycopy时间会增加,同时list元素也会减少!

最有效的移除方法是按降序移除元素:while(list.size()>0)list.remove(list.size()-1);//取o(1)while(list.size()>0)list.remove(0);//取o(阶乘(n))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//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毫秒
  • 描述索引:519毫秒---最佳
  • 对于迭代器:1043毫秒

这可能不是最好的方法,但对于大多数小型案例来说,这应该是可以接受的:

"create a second empty-array and add only the ones you want to keep"

我不记得从哪里读到的…为了公正起见,我会制作这个wiki,希望有人能找到它,或者只是为了不赢得我不应得的代表。


1
2
3
Collection<Integer> l = new ArrayList<Integer>();//Do the collection thing...

l.removeIf(i -> i == 5);      //iterates through the collection and removes every occurence of 5

JDK8中的lambda表达式和收集方法非常有用,并且添加了一些语法糖分??

方法removeIf通过集合循环,并使用谓词进行筛选。谓词是返回布尔值的参数的函数…就像boolean _bool = (str) -> str.equals("text");一样