首页 > 编程语言 >ConcurrentModificationException的解决,让你明白阅读源码的重要性!

ConcurrentModificationException的解决,让你明白阅读源码的重要性!

时间:2022-12-23 15:07:23浏览次数:47  
标签:遍历 迭代 ConcurrentModificationException ArrayList list remove add 源码 重要性


一. 背景

我们知道,有时候样本范围内的数据并不都是我们所需要的,某些情况下我们只需要其中的一部分。在这种情况下,我们在遍历样本时,就需要对取出的每一个样本数据进行判断,看看该样本是否满足我们的需要,对不需要的样本则要遍历删除。然而就是遍历删除这么简单的一件事情,我们在进行代码实现时也会暗含杀机,很多初学者在对集合进行遍历删除时会出现一些自己解决不了的异常。

举个例子,比如我们现在有一个List集合,样本数据是["a","b","b","c","d"],现在我们的需求是要将b这个元素从List集合中删除。当你看到这个需求时,脑海中是不是会觉得就这?这不就是一个简单的遍历删除吗?so easy!!然后你立马给出了N种实现方案,如下。

二. 实现方案一

很多小盆友给出的第一种解决方案,其实现代码如下

public static void main(String[] args) {

List<String> list = new ArrayList<String>(){{
add("a");
add("b");
add("b");
add("c");
}};

for(int i=0;i<list.size();i++){
String s=list.get(i);
if(s.equals("b"))
{
list.remove(s);
}
}

list.forEach( System.out::println);
}

但上面代码的执行结果,却会让你大跌眼镜,这个"b"怎么没删掉呢?如下图所示:

ConcurrentModificationException的解决,让你明白阅读源码的重要性!_bug

这是何解?

理论上来说,我们上面的代码执行逻辑可谓是天衣无缝,结果却大失所望。

其实之所以会出现这个问题,根源在于我们对数组的理解不够!我们知道数组是一片连续的内存空间,当我们从数组中移除元素时,数组是要进行内存拷贝保证内存连续性的,其源码如下:

ConcurrentModificationException的解决,让你明白阅读源码的重要性!_迭代器_02

 这里解释一下,当i=1时,会将“b”元素删除;将第一个“b”元素删除后,后面的元素就会填充到角标为1的位置;而这时候i++,i的值为2,所以第二个“b”元素就成功逃逸了。上面的b之所以删除不掉,问题就在这!那怎么解决这个坑呢?

其实很简单,咱们看看能不能从数组的最后一个元素开始遍历,代码改写如下:

public static void main(String[] args) {

List<String> list = new ArrayList<String>(){{
add("a");
add("b");
add("b");
add("c");
}};

for(int i = list.size()-1 ;i >= 0;i--){
String s=list.get(i);
if(s.equals("b")){
list.remove(s);
}
}

list.forEach( System.out::println);
}

此时再次执行的结果如下:

ConcurrentModificationException的解决,让你明白阅读源码的重要性!_迭代器_03

 漂亮,竟然解决了!那这次为什么就能解决呢?请你开动脑筋好好想想吧!!!

三. 实现方案二

1. 代码实现

除了上面的实现方式之外,有的朋友还给出了第二种实现思路,代码如下

public static void main(String[] args) {

List<String> list = new ArrayList<String>(){{
add("a");
add("b");
add("b");
add("c");
}};

for(String s:list){
if(s.equals("b")){
list.remove(s);
}
}

list.forEach( System.out::println);
}

上面的代码也很好理解,就是一个增强for循环。但该代码的执行结果更是让人大跌眼镜,不但没有把b元素给删除掉,人家竟然还抛异常了!他妹的,这是咋搞的?

ConcurrentModificationException的解决,让你明白阅读源码的重要性!_List_04

2. 原因剖析

这里壹哥要给大家解释一下,咱们要明白增强for循环的底层其实就是在使用迭代器进行遍历,这就相当于我们在代码中使用迭代器的遍历,但错就错在使用了ArrayList的remove方法进行删除。那为什么用迭代器遍历就不能使用ArrayList的remove方法进行删除呢?如果你不知道其中缘由,壹哥劝你还是要多读读源码,源码里面自有解答哦!

ConcurrentModificationException的解决,让你明白阅读源码的重要性!_机器学习_05

这是ArrayList的迭代器,有hasNext()、next()、remove()三个方法,这里我们重点看看next()方法的源码。next()方法的源码中,有一个check()检查,主要用于检查modCount与expectedModCount是否相等,如果不相等,就会直接抛出上面说到的并发修改异常。

ConcurrentModificationException的解决,让你明白阅读源码的重要性!_机器学习_06

那又为什么这两个变量会不相等呢?这就要对比迭代器的remove()和ArrayList的remove()方法了。在下图中,你看到了吗?迭代器的remove()方法中执行了expectedModCount = modCount操作,即让这两个变量相等,但ArrayList的remove()方法中却没有。

ConcurrentModificationException的解决,让你明白阅读源码的重要性!_迭代器_07

看到这个源码,我们应该就能想出解决上面异常的办法了吧!其实办法也很简单,如果我们要使用迭代器的遍历,那么删除时也要用迭代器的删除方法,而不能在用迭代器遍历时,使用ArrayList的remove()方法。即两者要配套,迭代器进行遍历,就用迭代器的删除。修改后的代码如下:

public static void main(String[] args) {
List<String> list = new ArrayList<String>(){{
add("a");
add("b");
add("b");
add("c");
}};


Iterator<String> it = list.iterator();
while (it.hasNext()){
String s = it.next();
if (s.equals("b")){
it.remove();
}
}

list.forEach( System.out::println);
}

上面代码的运行结果如下所示:

ConcurrentModificationException的解决,让你明白阅读源码的重要性!_机器学习_08

四. 小结

本文中,看似简单问题,却没想到后面会隐含着这么大的学问,所以各位小伙伴在平时的学习过程中,还是要多阅读源码,要不然你都不知道自己到底错在了哪里。对我们程序员来说,要做到”好读书,更求甚解“,这样才能让自己的水平变得更强!

 

标签:遍历,迭代,ConcurrentModificationException,ArrayList,list,remove,add,源码,重要性
From: https://blog.51cto.com/u_7044146/5965714

相关文章