三.层层揭秘,为什么发生了异常呢?
我们现在明白为什么阿里强制要求不要在foreach循环里面进行元素的remove/add操作,因为会发生异常了。
但是开发手册里面并没有告诉你,为什么会发生异常。需要我们自己层层深入,积极探索。
3.1 第一层:异常信息解读
所以这一小节我们就一起探索,为什么会发生异常。我们再解析一下程序的运行结果,如下:
正如上图里面异常信息的体现,异常是在代码的第21行触发的。而代码的第21行,是一个foreach循环。foreach循环是Java的语法糖,我们可以从编译后的class文件中看出,如下图所示:
请注意图中的第26行代码:
list.remove(item) (这句话很关键!!!)
很关键,很重要,后面会讲到。
这也解释了,异常信息里面的这一个问题:
好了,到这一步,我们把异常信息都解读完毕了。
3.2 第二层:抛出异常的条件解读
我再看看真实抛出异常的那一个方法:
很简单,很清晰的四行代码。抛出异常的条件是:
modCount !=expectedModCount
所以,我们需要解开的下两层面纱就是下面两大点:
第一:什么是modCount?它是干啥的?什么时候发生变化?
第二:什么是expectedModCount?它是干啥的?什么时候发生变化?
3.3 第三层:什么是modCount?它是干啥的?什么时候发生变化?
先来第一个:什么是modCount?
modCount上的注释很长,我只截取了最后一段。在这一段中,提到了两个关键点。
1.modCount这个字段位于java.util.AbstractList抽象类中。
2.modCount的注释中提到了"fail-fast"机制。
3.如果子类希望提供"fail-fast"机制,需要在add(int,E)方法和remove(int)方法中对这个字段进行处理。
4.从第三点我们知道了,在提供了"fail-fast"机制的容器中(比如ArrayList),除了文中示例的remove(Obj)方法会导致ConcurrentModificationException异常,add及其相关方法也会导致异常。
知道了什么是modCount。那modCount是干啥的呢?
在提供了"fail-fast"机制的集合中,modCount的作用是记录了该集合在使用过程中被修改的次数。
证据就在源码里面,如下:
这是java.util.ArrayList#add(int, E)方法的源码截图:
注:这里不讨论手动设置为null是否对GC有帮助,我个人认为,在这里有这一行代码并没有坏处。在实际开发过程中,一般不需要考虑到这点。
同时,上面的源码截图也回答了这一层的最后一个问题:它什么时候被修改?
拿ArrayList来说,当调用add相关和remove相关方法时,会触发modCount++操作,从而被修改。
好了,通过上面的分析,我们知道了什么是modCount和modCount是干啥的。准备进入第四层。
3.4 第四层:什么是expectedModCount?它是干啥的?什么时候发生变化?
接下来:什么是expectedModCount?
expectedModCount是ArrayList中一个名叫Itr内部类的成员变量。
第二问:expectedModCount它是干啥的:
它代表的含义是在这个迭代器中,预期的修改次数
第三问:expectedModCount什么时候发生变化?
情况一:从上图中也可以看出当Itr初始化的时候,会对expectedModCount字段赋初始值,其值等于modCount。
情况二:如下图所示,调用Itr的remove方法后会再次把modCount的值赋给expectedModCount。
换句话说就是:调用迭代器的remove会维护expectedModCount=modCount。(这句话很关键!!!)
好了分析到了这里,我们知道了下面这个六连击:
1.什么是modCount?
2.modCount是干啥的?
3.modCount什么时候发生变化?
4.什么是expectedModCount?
5.expectedModCount是干啥的?
6.expectedModCount什么时候发生变化?