List 集合遍历过程中删除元素避坑指南。

文章目录

  • [1. 遍历](#1. 遍历)
  • [2. 遍历过程中删除元素](#2. 遍历过程中删除元素)
    • [2.1 for 简单循环正向遍历方式](#2.1 for 简单循环正向遍历方式)
    • [2.2 for 简单循环反向遍历方式](#2.2 for 简单循环反向遍历方式)
    • [2.3 foreach 方式遍历删除](#2.3 foreach 方式遍历删除)
    • [2.4 Iterator的remove()方法](#2.4 Iterator的remove()方法)
    • [2.5 <font color = green> removeIf() (推荐)<green>](#2.5 removeIf() (推荐)<green>)
    • [2.6 Strem 方式](#2.6 Strem 方式)

作为一名后端开发,不管采用什么语言 使用 List 集合的频率都非常高。

对 List 集合的遍历和遍历中操作数据也是家常便饭。

我从我使用的过程中对于此问题的思考与实践形成记录,与大家交流。有不对的地方,恳请大家指正。

1. 遍历

List 集合的遍历有很多种方式,每一种遍历方式只有性能上的差异,不会有异常和暗坑。这里不再赘述。


2. 遍历过程中删除元素

因为 List 集合有多种遍历方式,也就意味着存在多种遍历中删除的方式:

在做各种方式比对前,我们先加入一些初始数据。

java 复制代码
 List<String> list = new ArrayList<>();
 list.add("snow");
 list.add("nier");
 list.add("sar");
 list.add("juaya");
 list.add("rock");
 list.add("snow");
 list.add("nier");

2.1 for 简单循环正向遍历方式

java 复制代码
for (int i = 0; i < list.size(); i++) {
	String name = list.get(i);
	if(name.equals("snow")){
		list.remove(i);
	}
	if(name.equals("nier")){
		list.remove(i);
	}
}

System.out.println(list);

上述代码打印结果:

bash 复制代码
 [nier, sar, juaya, rock, nier]

很明显 这不是正确的结果。

这是 因为:在遍历过程中,删除或者增加元素后,集合长度会因为实时改动而改动,也就是说集合在遍历过程中忘了初心了。

举个栗子:现在对长度为 5 的集合进行遍历,在遍历到 第三个元素的时候删除了ta,那么此时集合长度成了4,那么此时集合就不会遍历到之前下标为 5 的元素了。

知道了原因后,我们可以对症下药,达到我们的预期。修改代码如下:

java 复制代码
for (int i = 0; i < list.size(); i++) {
	String name = list.get(i);
	if(name.equals("snow")){
		list.remove(i);
		//	加补偿机制
		i--;
	}
	if(name.equals("nier")){
		list.remove(i);
		//	加补偿机制
		i--;
	}
}

System.out.println(list);

上述代码执行结果:

bash 复制代码
[sar, juaya, rock]

如此 加入了补偿机制后虽然达到了预期结果。但是很明显代码不够优雅。So 这种方式不推荐。


2.2 for 简单循环反向遍历方式

反向遍历和正向遍历类似,只不过是反方向的钟而已。

java 复制代码
for (int i = list.size() - 1; i >= 0; i--) {
	String name = list.get(i);
	if(name.equals("snow")){
		list.remove(i);
	}
	if(name.equals("nier")){
		list.remove(i);
	}
}

System.out.println(list);

上述代码执行结果:

bash 复制代码
[sar, juaya, rock]

咦~可以哎。结果正确。那这种方式是不是就绝对可以了呢。 NO!

逆序遍历虽然在这个场景下达到了预期的正确结果,但是因为 这种方式和正序在原理上是相同的。所以在遍历过程中操作元素也会有其劣根性。比如在逆序遍历过程中加入元素,依然会对整个遍历产生影响。 所以这也不是推荐的方式。


2.3 foreach 方式遍历删除

单从遍历来看 这种方式比上述两种方式优雅了不少。但是遍历过程中操作元素能达到预期结果吗?

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

执行结果...

What???


原因: 这种循环方式,其生成的字节码其实使用的 Iterator ,使用的核心方法是 `hasnext()` 和 `next()`。 然后再来看下ArrayList类的 Iterator 是如何实现的呢? Iterator 部分源码:

可以看出:调用next()方法获取下一个元素时,第一行代码就是调用了checkForComodification();

而该方法的核心逻辑就是比较 modCountexpectedModCount 这2个变量的值。

在上面的例子中,刚开始 modCountexpectedModCount 的值都为 n,所以第1次获取元素 "rock" 是没问题的,但是当执行完下面这行代码时:

java 复制代码
list.remove("rock");

modCount 的值就被修改了。

So: 使用 foreach 遍历过程中删除元素是不可行的。


2.4 Iterator的remove()方法

java 复制代码
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
    String next = iterator.next();
    if("snow".equals(next)){
        iterator.remove();
    }
}

执行结果:

bash 复制代码
[nier, sar, juaya, rock, nier]

很 nice !

那这种方式怎么就可以删除了呢???

因为:

这种方式每次删除一个元素,都会将modCount 的值重新赋值给 expectedModCount ,这样2个变量就相等了,不会触发 java.util.ConcurrentModificationException 异常。

虽然这种方式符合了业务要求,但是还是不推荐这种方式,因为他的写法依然不够优雅。


2.5 removeIf() (推荐)

从JDK1.8开始,可以使用 removeIf() 方法来代替 Iterator的remove()方法实现一边遍历一边删除。

java 复制代码
list.removeIf(s -> s.equals("snow"));

System.out.println(list);

打印结果:

bash 复制代码
[nier, sar, juaya, rock, nier]

其源码如下:


2.6 Strem 方式

java 复制代码
list = list.stream().filter( name -> !("snow".equals(name) ) ).collect(Collectors.toList());

System.out.println(list);

filter 过滤会保留满足条件的。

结果如下:

bash 复制代码
[nier, sar, juaya, rock, nier]

相关推荐
yang-2307几秒前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
沉登c1 分钟前
幂等性接口实现
java·rpc
代码之光_198013 分钟前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
丶Darling.28 分钟前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树
立秋678934 分钟前
Python的defaultdict详解
服务器·windows·python
labuladuo52039 分钟前
Codeforces Round 977 (Div. 2) C2 Adjust The Presentation (Hard Version)(思维,set)
数据结构·c++·算法
Indigo_code1 小时前
【数据结构】【链表代码】合并有序链表
数据结构·windows·链表
jiyisuifeng19911 小时前
代码随想录训练营第54天|单调栈+双指针
数据结构·算法
我言秋日胜春朝★1 小时前
【C++】红黑树
数据结构
科技资讯早知道1 小时前
java计算机毕设课设—坦克大战游戏
java·开发语言·游戏·毕业设计·课程设计·毕设