什么是fail-fast、fail-safe
fail-fast(快速失败) 和 fail-safe(安全失败) 是 Java集合中常见的错误检测机制,通常出现在集合遍历过程中。
fail-safe允许在遍历的过程中对容器中的数据进行修改,而fail-fast则不允许。
java.util下的所有集合类都是 fail-fast 机制,而 java.util.concurrent 包下的所有集合类都是 fail-safe机制。
java.util包下
ArrayList
Vector
HashMap
HashTable
可以看到在 java.util包下,无论你是线程安全的集合类还是不安全的集合类,在遍历过程中删除元素都会报 java.util.ConcurrentModificationException
异常 ,即并发修改异常。
为什么会报这种错误?
我们先来看看源码,以 ArrayList 为例
这个 modCount
变量 可能小伙伴们平时没有注意到过,这个变量其实是记录着的当前集合的一个修改次数,没调用一次add方法和remove方法的话,modCount就会加一。
说到这其实已经很明白了,但还要给大家说个知识点,那就是 foreach的原理,下面是代码及反编译之后的结果
foreach 底层原理
我们可以看到其实 foreach遍历其实优化成了调用迭代器遍历,说到这我再给大家说一说 list 的iterator方法吧
list的iterator()方法原理
总结一下吧
为什么一边遍历一遍删除会报错?
因为 list 内部存储一个名为 modCount的变量,它记录着集合的修改次数,而通过foreach遍历(即迭代器遍历)的时候,迭代器内部会维护一个初始值为0的游标 cursor,当cursor等于size的时候就会退出遍历,而迭代器每次调用next()方法时,会去调用一个checkForComodification()方法,而这个方法就是检查你的modCount的次数是否被改变,如果改变了说明被修改了则报错 ConcurrentModificationException;
java.util.concurrent包下
可以看到在 java.util.concurrent包下,在遍历过程中删除元素,都没有报错
如何解决
除了使用 java.util.concurrent 包下的集合类之外,还可以使用 迭代器进行遍历操作(当然如果实在多线程环境下还需要加锁)。
可以看见删除失败,成功遍历(要调用迭代器的 remove方法),而foreach之所以报错是因为还是调用的 list本身的remove方法,这就是区别。
为什么呢?
之前有一个字段 lastRet
我没有解释道,现在可以跟大家说,这个字段根据我的理解其实就是记录着当前元素的索引,我们可以看到 next()方法,开始用 i 记录游标,最后返回元素让 lastRet = i
因为返回的时当前元素,所以 lastRet 记录的也是当前元素的索引
再看 remove方法,由于lastRet保存着当前元素的索引(迭代器中没有remove(下标值)这个重载方法),所以直接调用list本身的remove方法传入下标值据可以删除了,而且删除之后更新 cursor、lastRet、和expectedModCount(调用list的removce方法modCount会++),这样的话对于下一次next方法来说没有什么影响,因为list本身的remove方法会涉及到元素的移动,
画图不够熟练,想明白还是很简单的,大家多看一下
顺带一提为什么java.util.concurrent包下的集合类也没报错,以CopyOnWriteArrayList为例吧
重点在remove方法
对于元素的删除操作是在新数组上完成的,所以当前集合不会修改,所以不会报错。
尾言
我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!
创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以一键三连哦!,感谢支持,我们下次再见~~~
分享大纲
更多精彩内容分享,请点击 Hello World (●’◡’●)