Java如何在遍历集合时删除特定元素

文章目录

环境

  • Ubuntu 24.04.1
  • Java 21

问题

创建一个List对象如下:

java 复制代码
        List<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        list.add("ddd");
        list.add("eee");

现在要遍历list,移除其中的 ccc 元素。

java 复制代码
        for (String s : list)
            if (s.equals("ccc"))
                list.remove(s);

运行代码,报错如下:

powershell 复制代码
Exception in thread "main" java.util.ConcurrentModificationException
	at 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"),则不会报错:

java 复制代码
        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() 方法时,都会检查 expectedModCountmodCount 是否相等,如果二者不相等,就会抛出 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() 方法来删除其当前指向元素:

java 复制代码
        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循环

这里又有个坑:

java 复制代码
        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) 获取下一个元素时,实际上获取的是下下一个元素(如果没想明白,单步调试一下就清楚了)。所以,这种方法只适用于删除一个元素的情况(当然,最好是压根就别用这种方法)。

正确方法是使用逆序遍历:

java 复制代码
        for (int i = list.size() - 1; i >= 0; i--) {
            var str = list.get(i);
            if (str.equals("ccc"))
                list.remove(str);
        }

使用逆序遍历,即使删除元素,也不影响后续的遍历(可保证每个元素遍历一次)。

removeIf()方法

java 复制代码
        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对象如下:

java 复制代码
        Set<String> set = new HashSet<>();
        set.add("aaa");
        set.add("bbb");
        set.add("ccc");
        set.add("ddd");
        set.add("eee");

Iterator

使用Iterator来遍历并删除特定元素:

java 复制代码
        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()方法

java 复制代码
        set.removeIf(str -> str.equals("bbb"));

Map

创建Map对象如下:

java 复制代码
        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值:

java 复制代码
        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());
            }
        }

或者:

java 复制代码
        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()方法

java 复制代码
        map.entrySet().removeIf(entry -> entry.getKey().equals("d"));

或者:

java 复制代码
        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() 处。

相关推荐
XQ丶YTY6 分钟前
大二java第一面小厂(挂)
java·开发语言·笔记·学习·面试
一零贰肆23 分钟前
深入理解SpringBoot中的SpringCache缓存技术
java·springboot·springcache·缓存技术
码上飞扬1 小时前
Java大师成长计划之第22天:Spring Cloud微服务架构
java·运维·云计算
秋野酱1 小时前
基于javaweb的SpringBoot自习室预约系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
面试官E先生2 小时前
【极兔快递Java社招】一面复盘|数据库+线程池+AQS+中间件面面俱到
java·面试
琢磨先生David2 小时前
构建优雅对象的艺术:Java 建造者模式的架构解析与工程实践
java·设计模式·建造者模式
小雅痞2 小时前
[Java][Leetcode simple]26. 删除有序数组中的重复项
java·leetcode
青云交2 小时前
Java 大视界 -- 基于 Java 的大数据分布式存储在工业互联网海量设备数据长期存储中的应用优化(248)
java·大数据·工业互联网·分布式存储·冷热数据管理·hbase 优化·kudu 应用
纸包鱼最好吃2 小时前
java基础-package关键字、MVC、import关键字
java·开发语言·mvc
唐山柳林2 小时前
城市生命线综合管控系统解决方案-守护城市生命线安全
java·安全·servlet