详解为什么在foreach中不能进行remove和add操作
今天早上小熙在关注的公众号中看到了这一篇不错的技术分享,特此实践顺便阐述下自己的理解。
在阿里开发手册上有这样一条规定:
阿里规范上没有给出详细解释。所以小熙就详细的说说我的借鉴和理解。
一. foreach循环
-
介绍:
Java5之后引入的功能。使得遍历数组和集合更加简洁,无需获得数组和集合长度,无需根据索引来访问数组元素和集合元素。foreach循环自动遍历数组和集合的每个元素。所以通常也被称为增强for循环。 -
语法:
for(type(元素类型) variableName(元素变量) : array | collection(遍历的容器对象)) { //variableName 自动迭代访问每个元素... }
-
演示foreach和for循环的遍历结果(其实是一样的)
(1)编写代码:
public static void main(String[] args) { // 普通for循环遍历 List<String> stringListFor = getStringList(); for (int i = 0; i < stringListFor.size(); i++) { System.out.println(stringListFor.get(i)); } System.out.println("-----------------华丽的分割线----------------------"); //增强for循环遍历 for (String s:getStringList() ) { System.out.println(s); } } private static List<String> getStringList(){ List<String> list = new ArrayList<>(); Collections.addAll(list,"程熙","yxg","chengxi","chengxi"); return list; }
(2)结果展示:
由上图可知,foreach循环和for循环的结果是一致的。而且更加简便。(3) foreach循环原理:
3.1 这里小熙使用的是命令行操作,使用javac命令编译BianLi.java文件,得到BianLi.class文件
$javac BianLi.java
3.2 使用javap命令,反编译BianLi.class文件
$ javap -v BianLi
3.3 将-v改为-c 输出分解后的代码,例如,类中每一个方法内,包含java字节码的指令,
$ javap -c BianLi
之后查看命令行中显示的代码,(注意这些操作文件中不能含中文,否则会报不可映射字符,小熙将必要的都改为了英文,这里只是验证foreach的底层是使用了迭代器后面会恢复)
如图:
由上图可以看出(小熙将华丽的分割线中文改为了fengexian全拼),foreach的底层就是迭代器实现的。
当然还有其他工具能更方便更清楚的展示,如使用jad工具,小熙只是想使用底层命令行实现并查看下。
二. 重审问题
规范中指出不让我们在foreach循环中对集合元素做add/remove操作,那么,我们尝试着做一下看看会发生什么问题。
- 修改如图:
- 结果如图:
由上图可知,普通for循环删除后没有报错(其实普通的for循环也不推荐,因为会有漏删,下面会详细介绍),而foreach循环删除元素确抛错:java.util.ConcurrentModificationException(修改并发异常),同理,增加也会抛这个错,那这是为什么呢,其实是因为触发了一个Java集合的错误检测机制——fail-fast 。
三. fail-fast
fail-fast,即快速失败,它是Java集合的一种错误检测机制。 当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。
同时需要注意的是,即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。
-
那这又和foreach循环有什么关系呢?
上文分析到了,foreach的底层是迭代器实现的,由debug发现,在迭代器中使用Iterator.next 会调用 Iterator.checkForComodification方法 ,而异常就是checkForComodification方法中抛出的。我们直接看下checkForComodification方法的代码,看下抛出异常的原因:
那下面就解释下吧。 -
解释
首先,我们要搞清楚的是,到底modCount和expectedModCount这两个变量都是个什么东西。通过翻源码,我们可以发现:
-
modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。
-
expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。
-
Itr是一个Iterator的实现,使用ArrayList.iterator方法可以获取到的迭代器就是Itr类的实例。
他们之间的关系如下:
3. 结论由上图可知,迭代器开始遍历之前该迭代器的expectedModCount (期望集合修改次数)已经被modCount(实际修改次数)赋值了,然而在foreach遍历中删除数据或添加数据都是对modCount(实际修改次数)的修改,又因为是调用集合类自己的方法,所以不会对expectedModCount (期望集合修改次数)进行修改(这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除或添加了),所以就造成了不等的情况,所以就会抛出一个java.util.ConcurrentModificationException(修改并发异常),用来提示用户,可能发生了并发修改。
四. 讨论下用法
-
接着说为什么正常的for循环不建议呢?
因为这会发生漏删的情况,如下图:
代码:
结果:
由上图可知我要删除集合中的chengxi,但是删除后还有,这就是漏删了,那这又是为什呢?因为普通for循环是根据索引删除的,由于两个相同值在相邻的位置,当删除第一个值之后,集合发生改变要重新排序索引(因为集合发生改变他的的底层**Object[]**要做位移操作,这里是要向前位移一个索引),所以后面那个要被删除的值就被挪到了删除值的索引位置,从而避免了删除也就造成了漏删。
当然也可以解决,在每次删除值的时候让索引自减就好了,最好是倒序遍历,还有equals方法也要倒着写防止空指针。
i--;
-
可以直接使用迭代器(会修改expectedModCount (期望集合修改次数))
代码:
// 迭代器循环遍历 List<String> stringListFor = getStringList(); System.out.println("删之前:"+stringListFor); Iterator<String> iterator = stringListFor.iterator(); while (iterator.hasNext()){ if(iterator.next().equals("chengxi")){ iterator.remove(); } }
结果:
-
可以使用java 8的新特性filter过滤不要的
代码:
// java 8新特性filter循环遍历 List<String> stringListFor = getStringList(); System.out.println("删之前:"+stringListFor); List<String> stringList8 = stringListFor.stream().filter(s -> !s.equals("chengxi")).collect(Collectors.toList()); System.out.println("删之后:"+stringList8);
结果:
-
其实foreach也可以,但是限制条件很多
首先你很明确只删除一个元素,而且删除之后就直接结束循环,避免下一次使用Iterator.next就不会抛出异常了。
代码:
//增强for循环遍历 List<String> stringListForeach = getStringList(); System.out.println("遍历之前"+stringListForeach); for (String s:stringListForeach ) { if(s.equals("程熙")){ stringListForeach.remove(s); } //删除后就结束遍历,避免抛异常 break; } System.out.println("遍历之后"+stringListForeach);
结果:
当然还有很多其他方法,小熙目前就介绍这么多了,有兴趣的朋友可以自行了解下。希望本文对大家有用,如有疑问可以点击小熙的头像获取小熙的联系方式和小熙联系哦。