本文已经收录到开源项目《大厂面试指北》,获取《大厂面试指北》离线PDF版,请关注公众号"大厂面试"
大厂面试公众号:
MySQL慢查询优化(线上案例调优)mp.weixin.qq.com![744a68e12bdf515328f242d4aceead66.png](https://i-blog.csdnimg.cn/blog_migrate/e45b4010a8c9b37df43a0c31a1d3a207.png)
《大厂面试指北》最佳预览地址:
《面试指北》notfound9.github.io《大厂面试指北》项目地址:
https://github.com/NotFound9/interviewGuidegithub.com![bb974370fd061c9626bd2e935de4b5ea.png](https://i-blog.csdnimg.cn/blog_migrate/a7533b3ea63d61e78ee52a8b085da39b.jpeg)
简介
我们在项目开发过程中,经常会有需求需要删除ArrayList中的某个元素,而使用不正确的删除方式,就有可能抛出异常。或者在面试中,会遇到面试官询问遍历时如何正常删除元素。所以在本篇文章中,我们会对几种删除元素的方式进行测试,并对原理进行研究,希望可以帮助到大家!
ArrayList遍历时删除元素的几种姿势
首先结论如下:
第1种方法 - 普通for循环正序删除(结果:会漏掉元素判断)
第2种方法 - 普通for循环倒序删除(结果:正确删除)
第3种方法 - for-each循环删除(结果:抛出异常)
第4种方法 - Iterator遍历,使用ArrayList.remove()删除元素(结果:抛出异常)
第5种方法 - Iterator遍历,使用Iterator的remove删除元素(结果:正确删除)
下面让我们来详细探究一下原因吧!
首先初始化一个数组arrayList,假设我们要删除等于3的元素。
public
第1种方法 - 普通for循环正序删除(结果:会漏掉元素判断)
for
输出结果:
当前arrayList是[1, 2, 3, 3, 4, 5]
当前arrayList是[1, 2, 3, 3, 4, 5]
当前arrayList是[1, 2, 3, 4, 5]
当前arrayList是[1, 2, 3, 4, 5]
当前arrayList是[1, 2, 3, 4, 5]
可以看到少删除了一个3,
原因在于调用remove删除元素时,remove方法调用System.arraycopy()方法将后面的元素移动到前面的位置,也就是第二个3会移动到数组下标为2的位置,而在下一次循环时,i+1之后,i会为3,不会对数组下标为2这个位置进行判断,所以这种写法,在删除元素时,被删除元素a的后一个元素b会移动a的位置,而i已经加1,会忽略对元素b的判断,所以如果是连续的重复元素,会导致少删除。
解决方案
可以在删除元素后,执行i=i-1,使得下次循环时再次对该数组下标进行判断。
第2种方法 - 普通for循环倒序删除(结果:正确删除)
for
输出结果:
当前arrayList是[1, 2, 3, 3, 4, 5]
当前arrayList是[1, 2, 3, 3, 4, 5]
当前arrayList是[1, 2, 3, 4, 5]
当前arrayList是[1, 2, 4, 5]
当前arrayList是[1, 2, 4, 5]
当前arrayList是[1, 2, 4, 5]
这种方法可以正确删除元素,因为调用remove删除元素时,remove方法调用System.arraycopy()将被删除元素a后面的元素向前移动,而不会影响元素a之前的元素,所以倒序遍历可以正常删除元素。
第3种方法 - for-each循环删除(结果:抛出异常)
public
输出结果:
当前arrayList是
会抛出ConcurrentModificationException异常,主要在于for-each的底层实现是使用ArrayList.iterator的hasNext()方法和next()方法实现的,我们可以使用反编译进行验证,对包含上面的方法的类使用以下命令反编译验证
javac
得到removeWayThree方法的反编译代码如下:
public
可以很清楚得看到Iterator.hasNext()来判断是否还有下一个元素,和Iterator.next()方法来获取下一个元素。而因为在删除元素时,remove()方法会调用fastRemove()方法,其中会对modCount+1,代表对数组进行了修改,将修改次数+1。
public
而当删除完元素后,进行下一次循环时,会调用下面源码中Itr.next()方法获取下一个元素,会调用checkForComodification()方法对ArrayList进行校验,判断在遍历ArrayList是否已经被修改,由于之前对modCount+1,而expectedModCount还是初始化时ArrayList.Itr对象时赋的值,所以会不相等,然后抛出ConcurrentModificationException异常。
那么有什么办法可以让expectedModCount及时更新呢?
可以看到下面Itr的源码中,在Itr.remove()方法中删除元素后会对 expectedModCount更新,所以我们在使用删除元素时使用Itr.remove()方法来删除元素就可以保证expectedModCount的更新了,具体看第5种方法。
private
第4种方法 - Iterator遍历,使用ArrayList.remove()删除元素(结果:抛出异常)
Iterator
输出结果:
当前arrayList是
第3种方法在编译后的代码,其实是跟第4种是一样的,所以第四种写法也会抛出ConcurrentModificationException异常。这种需要注意的是,每次调用iterator的next()方法,会导致游标向右移动,从而达到遍历的目的。所以在单次循环中不能多次调用next()方法,不然会导致每次循环时跳过一些元素,我在一些博客里面看到了一些错误的写法,比如这一篇《在ArrayList的循环中删除元素,会不会出现问题?》文章中:
[图片上传失败...(image-9f1ff1-1578232707248)]
先调用iterator.next()获取元素,与elem进行比较,如果相等,再调用list.remove(iterator.next());来移除元素,这个时候的iterator.next()其实已经不是与elem相等的元素了,而是后一个元素了,我们可以写个demo来测试一下
ArrayList
输出结果如下:
[
可以看到移除的元素其实不是3,而是3之后的元素,因为调用了两次next()方法,导致游标多移动了。所以应该使用Integer value = iterator.next();将元素取出进行判断。
第5种方法 - Iterator遍历,使用Iterator的remove删除元素(结果:正确删除)
Iterator
输出结果:
当前arrayList是
可以正确删除元素。
跟第3种和第4种方法的区别在于是使用iterator.remove();来移除元素,而在remove()方法中会对iterator的expectedModCount变量进行更新,所以在下次循环调用iterator.next()方法时,expectedModCount与modCount相等,不会抛出异常。
HashMap遍历时删除元素的几种姿势
首先结论如下:
第1种方法 - for-each遍历HashMap.entrySet,使用HashMap.remove()删除(结果:抛出异常)。
第2种方法-for-each遍历HashMap.keySet,使用HashMap.remove()删除(结果:抛出异常)。
第3种方法-使用HashMap.entrySet().iterator()遍历删除(结果:正确删除)。
下面让我们来详细探究一下原因吧!
HashMap的遍历删除方法与ArrayList的大同小异,只是api的调用方式不同。首先初始化一个HashMap,我们要删除key包含"3"字符串的键值对。
HashMap
第1种方法 - for-each遍历HashMap.entrySet,使用HashMap.remove()删除(结果:抛出异常)
for (Map.Entry<String,Integer> entry: hashMap.entrySet()) {
String key = entry.getKey();
if(key.contains("3")){
hashMap.remove(entry.getKey());
}
System.out.println("当前HashMap是"+hashMap+" 当前entry是"+entry);
}
输出结果:
当前HashMap是
第2种方法-for-each遍历HashMap.keySet,使用HashMap.remove()删除(结果:抛出异常)
Set
输出结果如下:
当前HashMap是
第3种方法-使用HashMap.entrySet().iterator()遍历删除(结果:正确删除)
Iterator
输出结果:
当前HashMap是
第1种方法和第2种方法抛出ConcurrentModificationException异常与上面ArrayList错误遍历-删除方法的原因一致,HashIterator也有一个expectedModCount,在遍历时获取下一个元素时,会调用next()方法,然后对 expectedModCount和modCount进行判断,不一致就抛出ConcurrentModificationException异常。
abstract
PS:ConcurrentModificationException是什么?
根据ConcurrentModificationException的文档介绍,一些对象不允许并发修改,当这些修改行为被检测到时,就会抛出这个异常。(例如一些集合不允许一个线程一边遍历时,另一个线程去修改这个集合)。
一些集合(例如Collection, Vector, ArrayList,LinkedList, HashSet, Hashtable, TreeMap, AbstractList, Serialized Form)的Iterator实现中,如果提供这种并发修改异常检测,那么这些Iterator可以称为是"fail-fast Iterator",意思是快速失败迭代器,就是检测到并发修改时,直接抛出异常,而不是继续执行,等到获取到一些错误值时在抛出异常。
异常检测主要是通过modCount和expectedModCount两个变量来实现的,
- modCount 集合被修改的次数,一般是被集合(ArrayList之类的)持有,每次调用add(),remove()方法会导致modCount+1
- expectedModCount 期待的modCount,一般是被Iterator(ArrayList.iterator()方法返回的iterator对象)持有,一般在Iterator初始化时会赋初始值,在调用Iterator的remove()方法时会对expectedModCount进行更新。(可以看看上面的ArrayList.Itr源码)
然后在Iterator调用next()遍历元素时,会调用checkForComodification()方法比较modCount和expectedModCount,不一致就抛出ConcurrentModificationException。
单线程操作Iterator不当时也会抛出ConcurrentModificationException异常。(上面的例子就是)
总结
因为ArrayList和HashMap的Iterator都是上面所说的“fail-fast Iterator”,Iterator在获取下一个元素,删除元素时,都会比较expectedModCount和modCount,不一致就会抛出异常。
所以当使用Iterator遍历元素(for-each遍历底层实现也是Iterator)时,需要删除元素,一定需要使用 Iterator的remove()方法 来删除,而不是直接调用ArrayList或HashMap自身的remove()方法,否则会导致Iterator中的expectedModCount没有及时更新,之后获取下一个元素或者删除元素时,expectedModCount和modCount不一致,然后抛出ConcurrentModificationException异常。