Iterating through a Collection, avoiding ConcurrentModificationException when removing objects in a loop
我们都知道你不能这样做:
1 2 3 4 5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
当然,这会导致:
1 |
…即使多个线程没有这样做…不管怎样。
这个问题的最佳解决方案是什么?如何在循环中从集合中移除项而不引发此异常?
我也在使用一个任意的
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(); } } |
请注意,
来源:docs.oracle>采集界面
同样,如果您有一个
在您的情况下,您试图从列表中删除,但如果在迭代其内容时尝试将
这工作:
1 2 3 4 5 6 | Iterator<Integer> iter = l.iterator(); while (iter.hasNext()) { if (iter.next() == 5) { iter.remove(); } } |
我假设既然foreach循环是迭代的语法甜头,使用迭代器没有帮助…但它提供了这个
使用Java 8,您可以使用新的EDCOX1×16的方法。应用于您的示例:
1 2 3 4 | Collection<Integer> coll = new ArrayList<>(); //populate coll.removeIf(i -> i == 5); |
由于问题已经得到了回答,即最好的方法是使用迭代器对象的remove方法,因此我将详细介绍抛出错误
每个集合类都有一个私有类,它实现迭代器接口,并提供诸如
下一步的代码如下所示…
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(); } } |
在这里,方法
1 2 3 4 | final void checkForComodification() { if (modCount != expectedModCount) throw new 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集合),在可变集合上定义的方法
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 } } |
不是你不能从迭代的
抱歉,如果这个答案是一个有点专业的用例,并且更适合我从这里到达的原始线程,那么这个线程被标记为这个线程的副本(尽管这个线程看起来更细微),并被锁定。
带有传统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++; } |
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 |
另一种方法是创建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 |
如果跳过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 |
我知道这个问题假设的只是一个
*更新:请参阅下面的注释,说明传统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。
在这里,我们看到循环不是从一开始就开始,也不是在每个元素上停止(我相信
fwiw我们还看到
fwiww相同的代码在修改后仍然有效,从每个元素的停止处开始(就像
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是最后一个元素的位置),则在没有
如果(索引减少),则arraycopy时间会增加,同时list元素也会减少!
最有效的移除方法是按降序移除元素:
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表达式和收集方法非常有用,并且添加了一些语法糖分??
方法