循环遍历集合,在循环中删除对象时避免并发修改异常

我们都知道你不能做以下事情,因为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,不一定是ArrayList,所以你不能依赖get

513056 次浏览

这工作:

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

我假设由于Foreach循环是迭代的语法糖,使用迭代器没有帮助……但它为您提供了这个.remove()功能。

#0是安全的,你可以这样使用它:

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

请注意,#0是在迭代期间修改集合的唯一安全方法;如果在迭代进行时修改了基础集合以任何其他方式,则该行为未指定。

来源: docs.oracle>收藏界面


类似地,如果你有ListIterator并且想要添加项,你可以使用#1,出于同样的原因,你可以使用Iterator#remove-它的设计允许它。


在您的示例中,您尝试从列表中删除,但如果在迭代其内容时尝试将put转换为Map,则相同的限制也适用。

你可以像你提到的那样直接使用迭代器,或者保留第二个集合并将你要删除的每个项目添加到新集合中,然后在最后删除所有。这允许你继续使用for-each循环的类型安全,但代价是增加内存使用和CPU时间(应该不是一个大问题,除非你有非常非常大的列表或非常旧的计算机)

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

由于问题已经回答过了,即最好的方法是使用迭代器对象的删除方法,我将详细介绍抛出错误"java.util.ConcurrentModificationException"的地方。

每个集合类都有一个私有类,它实现了Iterator接口并提供next()remove()hasNext()等方法。

Next的代码看起来像这样…

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

因此,如您所见,如果您显式尝试从集合中删除一个元素。它会导致modCountexpectedModCount不同,从而导致异常ConcurrentModificationException

复制现有列表并遍历新副本。

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

对于Eclipse集合,在不可用集合上定义的方法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);

使用Java8 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(),因为在Java8的java.util.Collection接口上添加了默认的removeIf方法。

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

克劳迪斯相同的答案,带有for循环:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {Object object = it.next();if (test) {it.remove();}}

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

msgstr"创建第二个空数组并只添加你想保留的数组"

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

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

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

这将避免并发异常。

使用Java8,您可以使用新的#0方法。应用于您的示例:

Collection<Integer> coll = new ArrayList<>();//populate
coll.removeIf(i -> i == 5);

在这种情况下,一个常见的技巧是(是?)向后走:

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

也就是说,我很高兴你在Java8中有更好的方法,例如流上的removeIffilter

在caseArrayList:删除(int index)-if(index是最后一个元素的位置)中,它避免没有System.arraycopy()并且不花时间。

数组复制时间增加如果(索引减少),顺便说一下列表的元素也减少!

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

//region prepare dataArrayList<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 indexmillis = 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 descmillis = 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 iteratormillis = 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 msec---最好的
  • 对于迭代器:1043毫秒

并发哈希表并发链接队列并发跳过列表映射可能是另一个选项,因为即使您删除或添加项目,它们也永远不会抛出任何并发修改异常。

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

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

干杯!

使用传统的for循环

ArrayList<String> myArray = new ArrayList<>();
for (int i = 0; i < myArray.size(); ) {String text = myArray.get(i);if (someCondition(text))myArray.remove(i);elsei++;}

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

人们正在断言从由Foreach循环迭代的集合中删除一个不能。我只想指出技术上是不正确的,并准确描述(我知道OP的问题是如此先进,以至于可以避免知道这一点)该假设背后的代码:

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

并不是说你不能从迭代的Colletion中删除,而是说一旦你这样做了,你就不能继续迭代。因此,上面代码中的break

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

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

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

线程安全集合修改示例:

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

我知道这个问题太老了,不能用Java8,但是对于那些使用Java8的人,你可以很容易地使用demveif():

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。但是对于那些阅读这个问题的人来说,如果你想避免#5,你可以用#4循环来避免#3(同时在其中修改)(如果你想一般地避免它,或者特别避免它以实现不同于在每个元素处开始到结束停止的循环顺序[我相信这是Iterator本身能做的唯一顺序]):

*更新:请参阅下面的评论,说明类似的也可以使用传统的-for-loop实现。

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

该代码中没有并发修改异常。

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

FWIW我们还看到get被调用在list上,如果它的引用只是Collection(而不是更具体的CollectionList类型),这是无法完成的-List接口包括get,但Collection接口没有。如果没有这种差异,那么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;}}

另一种方法是使用arrayList的副本进行迭代:

List<Object> l = ...    
List<Object> iterationList = ImmutableList.copyOf(l);    
for (Object curr : iterationList) {if (condition(curr)) {l.remove(curr);}}

一种解决方案可以是旋转列表并删除第一个元素以避免出现并发修改异常或IndexOutOfBoundsException

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

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

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

也可以使用递归

java中的递归是一个方法不断调用自己的过程。java中调用自己的方法称为递归方法。

现在,您可以使用以下代码删除

l.removeIf(current -> current == 5);

你可以使用一个同时循环。

Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();while(iterator.hasNext()){Map.Entry<String, String> entry = iterator.next();if(entry.getKey().equals("test")) {iterator.remove();}}

Java并发修改异常

  1. 单线程
Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {String value = iter.next()if (value == "A") {list.remove(it.next()); //throws ConcurrentModificationException}}

解决方案:迭代器remove()方法

Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {String value = iter.next()if (value == "A") {it.remove()}}
  1. 多线程

在使用stream().map()方法迭代列表时,我最终得到了这个ConcurrentModificationException。然而,for(:)在迭代和修改列表时没有抛出异常。

这是代码片段,如果对任何人有帮助:这里我迭代ArrayList<BuildEntity>,并使用list.remove(obj)

 for(BuildEntity build : uniqueBuildEntities){if(build!=null){if(isBuildCrashedWithErrors(build)){log.info("The following build crashed with errors ,  will not be persisted -> \n{}",build.getBuildUrl());uniqueBuildEntities.remove(build);if (uniqueBuildEntities.isEmpty()) return  EMPTY_LIST;}}}if(uniqueBuildEntities.size()>0) {dbEntries.addAll(uniqueBuildEntities);}

如果使用HashMap,在较新版本的Java(8+)中,您可以选择3个选项:

public class UserProfileEntity {private String Code;private String mobileNumber;private LocalDateTime inputDT;// getters and setters here}HashMap<String, UserProfileEntity> upMap = new HashMap<>();

// remove by valueupMap.values().removeIf(value -> !value.getCode().contains("0005"));
// remove by keyupMap.keySet().removeIf(key -> key.contentEquals("testUser"));
// remove by entry / key + valueupMap.entrySet().removeIf(entry -> (entry.getKey().endsWith("admin") || entry.getValue().getInputDT().isBefore(LocalDateTime.now().minusMinutes(3)));

当另一个线程也修改集合时,迭代器并不总是有帮助。我尝试了很多方法,但后来意识到手动遍历集合要安全得多(向后删除):

for (i in myList.size-1 downTo 0) {myList.getOrNull(i)?.also {if (it == 5)myList.remove(it)}}