文章目录
- 环境
- 问题
- 分析
- 解决办法
- Iterator
- for循环
- removeIf()方法
- 其它
- Set
- Iterator
- removeIf()方法
- Map
- Iterator
- removeIf()方法
- 总结
环境
- Ubuntu 24.04.1
- Java 21
问题
创建一个List对象如下:
List<String> list = new ArrayList<>();list.add("aaa");list.add("bbb");list.add("ccc");list.add("ddd");list.add("eee");
现在要遍历list,移除其中的 ccc
元素。
for (String s : list)if (s.equals("ccc"))list.remove(s);
运行代码,报错如下:
Exception in thread "main" java.util.ConcurrentModificationExceptionat java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)at org.example.Test0313.main(Test0313.java:25)
这里,抛出了 ConcurrentModificationException
(注意这个异常是在遍历(即for循环处)时抛出的,而不是在remove时抛出的)。
然而,神奇的是,如果删除的是list里倒数第2个元素(本例是 "ddd"
),则不会报错:
for (String s : list) {if (s.equals("ddd"))list.remove(s);
// System.out.println(s);}
能够正确的删除 "ddd"
。
本例中list里共有5个元素,经测试,如果是其它数量,也是只有在删除倒数第2个元素时,不会抛出异常。
分析
首先需要强调的是,要遍历List并删除特定的元素,不要用 for-each 循环的方式,否则绝大部分情况下都会抛出 ConcurrentModificationException
异常。删除倒数第2个元素时不报错,只是一个特例。
简而言之,for-each循环,强调的是“遍历”,而不是“增删改”(改还好,主要是增删)。这一点和Stream倒有点类似。
注:跑个题,Stream的 toList()
方法,会将Stream收集为一个“不可变List”,如果尝试添加或删除元素,会抛出 UnsupportedOperationException
异常,具体可参见我领一篇文档( https://blog.csdn.net/duke_ding2/article/details/143888158
)。
所以,实际应用时,只需记得:若要增删元素,则不要使用 for-each 循环的方式。
那么,到底为什么删除倒数第2个元素时不报错呢?
for-each循环本质上是使用迭代器(Iterator)来实现的。在迭代过程中,迭代器内部维护了一个 expectedModCount
变量,它记录了迭代器创建时列表的修改次数;而列表本身也有一个 modCount
变量,用于记录列表结构的修改次数。每次调用迭代器的 next()
方法时,都会检查 expectedModCount
和 modCount
是否相等,如果二者不相等,就会抛出 ConcurrentModificationException
异常。
所以,问题的关键点就在于Iterator的 next()
方法。显然,在删除倒数第2个元素后,并没有调用 next()
方法,所以不报错。
那么问题又来了,为什么在删除倒数第2个元素后,并没有调用 next()
方法;而在删除最后一个元素后,反而会调用 next()
方法呢(都遍历完了还next啥呢)?
判断是否需要调用Iterator的 next()
方法的条件是:Iterator的 hasNext()
方法返回true。
Iterator在判断 hasNext()
时,是通过List大小来判断的,遍历到倒数第2个元素并将其删除后,已遍历的元素数量和List的大小相等,Iterator会认为已经遍历结束了,所以 hasNext()
方法会返回false。
同理,遍历到倒数最后一个元素并将其删除后,已遍历的元素数量和List的大小并不相等,Iterator会认为已经遍历尚未结束,所以 hasNext()
方法会返回true。
这就有点扯淡了,已遍历的元素数量都超过List大小了,还觉得需要继续遍历呢?
Anyway,可能Java就是这么设计的:前面已经提到,不要这么做,否则会出问题,而你非要这么做,那不管出不出问题的,反正就是你的用法不对。 😃
总结:在for-each循环里删除元素会报错(特例:删除倒数第2个元素不报错),所以不要这么做。
解决办法
Iterator
前面是隐式使用Iterator,实际上,显式使用Iterator时,可以直接安全的通过其 remove()
方法来删除其当前指向元素:
Iterator<String> it = list.iterator();while (it.hasNext()) {String str = it.next();if (str.equals("ccc")) {it.remove();
// list.remove(str);}}
注意:使用Iterator时,不要通过List来删除元素(参见注释处代码),否则会遇到相同的问题(删除倒数第2个元素是OK的,但删除其它元素后,随后的 next()
方法会报错)。
for循环
这里又有个坑:
for (int i = 0; i < list.size(); i++) {var str = list.get(i);if (str.equals("ccc"))list.remove(str);}
看上去似乎一切OK,确实把 ccc
删除了。但是,这种方法有个潜在的问题:删除某个元素后,下一次迭代会跳过一个元素。这是因为删除元素后,其后的元素位置已经变化了,通过 list.get(i)
获取下一个元素时,实际上获取的是下下一个元素(如果没想明白,单步调试一下就清楚了)。所以,这种方法只适用于删除一个元素的情况(当然,最好是压根就别用这种方法)。
正确方法是使用逆序遍历:
for (int i = list.size() - 1; i >= 0; i--) {var str = list.get(i);if (str.equals("ccc"))list.remove(str);}
使用逆序遍历,即使删除元素,也不影响后续的遍历(可保证每个元素遍历一次)。
removeIf()方法
list.removeIf(e -> e.equals("ccc") || e.equals("ddd"));
注意:该方法简单又方便,推荐使用。
其它
List是有序集合,可通过位置来遍历,而对于Map、Set等无序集合,不能通过位置来遍历,只能通过Iterator或者for-each循环遍历。前面提到,不要在for-each循环里删除元素(当然可以变通一下,用for-each来遍历集合,然后重新生成一个满足需求的集合)。所以,一般用Iterator或者 removeIf()
方法来删除元素。(注:Map本身没有 removeIf()
方法,不过其entrySet和keySet都有 removeIf()
方法)。
Set
创建Set对象如下:
Set<String> set = new HashSet<>();set.add("aaa");set.add("bbb");set.add("ccc");set.add("ddd");set.add("eee");
Iterator
使用Iterator来遍历并删除特定元素:
Iterator<String> iterator = set.iterator();while (iterator.hasNext()) {var str = iterator.next();if (str.equals("eee")) {iterator.remove();
// set.remove(str);}}
注意:同理,不要在遍历时,使用Set的 remove()
方法删除元素(参见注释处代码),否则随后的 next()
方法会报错。同样,也是在删除倒数第2个元素时不报错,只不过Set是无序的,哪个元素是倒数第2个元素,取决于Set内部实现。
removeIf()方法
set.removeIf(str -> str.equals("bbb"));
Map
创建Map对象如下:
Map<String, String> map = new HashMap<>();map.put("a", "aaa");map.put("b", "bbb");map.put("c", "ccc");map.put("d", "ddd");map.put("e", "eee");
Iterator
使用Iterator来遍历并删除特定key值:
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<String, String> entry = iterator.next();if (entry.getKey().equals("d")) {iterator.remove();
// map.remove(entry.getKey());}}
或者:
Iterator<String> iterator = map.keySet().iterator();while (iterator.hasNext()) {String key = iterator.next();if (key.equals("c")) {iterator.remove();
// map.remove(key);}}
同理,不要在遍历时,使用Map的 remove()
方法删除key值(参见注释处代码),否则随后的 next()
方法会报错。同样,也是在删除倒数第2个key值时不报错,只不过keySet是无序的,哪个key值是倒数第2个key值,取决于keySet内部实现。
removeIf()方法
map.entrySet().removeIf(entry -> entry.getKey().equals("d"));
或者:
map.keySet().removeIf(key -> key.equals("c"));
总结
对于List、Set、Map:
- 不要在for-each循环里删除元素,否则会抛出
ConcurrentModificationException
异常(例外:删除倒数第2个元素时不报错) - 推荐使用
removeIf()
方法来删除特定元素 - 也可以使用Iterator来遍历集合,并使用Iterator的
remove()
方法来删除元素 - 不要在使用Iterator遍历集合时,使用集合的
remove()
方法来删除元素(和第一条同理,实际上第一条就是隐含使用了Iterator) - 对于List,也可以用for循环,通过位置来删除元素,不过要注意需要倒序遍历List
注意:发生异常时,不是在删除元素处,而是在Iterator的 next()
处。