从List.remove()不抛ConcurrentModificationException看fail-fast机制
起因于这篇文章中的一段代码,代码有没有问题,会不会报错?
public class CollectionDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("1");
list.add("3");
list.add("5");
for (Object o : list) {
if ("3".equals(o))
list.remove(o);
}
System.out.println(list);
}
}
References:
- https://stackoverflow.com/questions/8189466/why-am-i-not-getting-a-java-util-concurrentmodificationexception-in-this-example
- https://segmentfault.com/a/1190000020086031?utm_source=tag-newest
- https://www.cnblogs.com/gxyandwmm/p/9642953.html
- https://blogs.oracle.com/corejavatechtips/using-enhanced-for-loops-with-your-classes
- https://docs.oracle.com/javase/1.5.0/docs/guide/language/foreach.html
- https://juejin.im/post/5cdf98c751882525b40cc991#heading-1
- https://stackoverflow.com/questions/8189466/why-am-i-not-getting-a-java-util-concurrentmodificationexception-in-this-example
看下上面代码执行的结果:
可以看到,没有报错,成功执行了,而且运行结果也是正确的。
那我们稍微改下这段代码再看下:
这样是不是就熟悉多了?和预期一样,抛出了ConcurrentModificationException异常。要理解为什么上面那段代码为什么不报错,就得仔细了解下fail-fast的机制了。先将这段代码反编译,看下增强for循环的语法糖背后实际执行代码:
很明显了,其实就是获取了iterator再遍历。到这其实问题就可以简化为“为什么fail-fast失效了”。先去刚刚报错的ArrayList中看下:
文档中对modCount字段的注释为:
The number of times this list has been structurally modified.Structural modifications are those that change the size of thelist, or otherwise perturb it in such a fashion that iterations inprogress may yield incorrect results.
...
简单理解为更改了集合的size就得更改此值,否则会导致迭代中的集合产生错误的结果。例如add或者remove之类的方法都会改变modCount。
查看checkForComodification方法,检测也很简单,如果预期的修改次数和实际的修改次数不一致就会抛出异常。在添加完所有元素准备循环时,先调用了获取迭代器方法,初始化时将modCount赋值给expectedModCount,这样在循环过程中预期的修改次数如果和实际的修改次数不一致,就说明在迭代过程中集合被修改了,直接抛异常就完了。
现在来看下卡头那段不抛异常的代码在remove("3")之后的一次循环检测:
内部迭代器中的cursor表示下一个要返回的元素,lastRet顾名思义就是上一个返回的元素。在没有remove之前,size是3,remove之后size变成了2,而cursor此时指向的就是最后一个元素,因此hasNext()返回false,循环也结束了。由于增强for循环的迭代器是隐藏的,已经没有机会再调用checkForComodification方法了,也就不会抛出异常。
根据以上,绕过fail-fast机制的条件很容易触发,删除集合中倒数第二个元素就会出现这种情况,因此在迭代中操作集合一定要用迭代器的方法,迭代器中的操作会修改expectedModCount为正确的值。
在我找资料的时候发现,原来这一条早已经写进了阿里的那个开发手册中,汗。虽然平时知道要用迭代器,但从没想过底层的实现是这样的。