深入理解Java集合中的Iterator
总结自掘金的一篇文章:深入理解Java集合中的Iterator
首先上一段面试题代码:
1 | public static void main(String[] args) { |
问:这段代码会不会报错呢?
这段代码看起来挺平常的,我是没看出来啥问题(还是菜)。
但是我们首先要知道的点:
- foreach 遍历底层是用迭代器来实现的
- 如果用迭代器遍历集合元素时,如果要删除或修改集合中元素,就必须使用迭代器的方法,不能用集合自身的方法
所以根据上面的知识点,这段代码应该是会报错的,但是一运行:
没一点毛病,换个数试试?
1 | public static void main(String[] args) { |
再次运行:
哦豁,就换了个数程序又报错了
既然是根据那篇文章总结,我们就直接了当的概况一下,总结一下大佬们的debug处理方式:
- 根据现有知识寻找可能出错的点
- 当出现 bug 时,先从异常入手,分析这个异常是如何发生的(一看官方文档二看源码)
- 知道了异常的大概发生场景后,看控制台报错的行数,找到具体异常位置
- 接着就是一层层的往里深入,打断点不断调试,解决问题
涉及知识
ConcurrentModificationException
- 这个异常会在一个对象被做了不合法的并发修改时抛出(单线程下也会出现)
fail-fast
- 集合中通常会自带一个 fail-fast 的迭代器
- 所谓 fail-fast 就是检测到有异常越快抛出异常结束越好,避免了未知的隐患
上图就是在 ArrayList 类中的迭代器,控制台的报错信息指向了里面 next 方法的 checkForComodification() 方法
这个方法也很简单,就是验证了 modeCount 和 expectedModCount 是否一致,不一致则异常
modCount
- modCount是定义在AbstractList(ArrayList的父类)里面的一个属性。
- 这个字段用来记录 list 被结构性操作的次数,就是对 list 容量有影响或者迭代过程中会导致错误的操作。
- 如果想要实现 fail-fast 那就要在所有结构性操作的方法内部做 modCount++ 的操作
- 如果不想实现 fail-fast 就不需要,ArrayList 里的 add 方法就实现了 modCount++ 操作
expectedModCount
- 这个值在上面已经出现过了,在 ArrayList 的迭代器中会将 expectedModCount 的值初始化为 modCount 的值
也就是说,在正常情况下,expectedModCount 和 modCount 的值是一致的,但是为什么会抛出不一致的异常呢?
打两个断点开始 debug :
因为刚开始 list 的 size 为 6 ,而 foreach 底层就是迭代器嘛,他会先判断 hasNext() , cursor 是当前迭代器的游标,游标没有到 size 大小就会返回 true ,继续执行 next 方法,而 next 方法刚上来第一步就是检查一致性,因为我们初始化后没有再在结构性上动,所以一直保持一致,当我们迭代到 6 时,执行 remove 操作
上来就是修改 modCount++ ,所以这是 expectedModCount 为 6 ,而 modCount 为 7。迭代器继续迭代,cursor != size,进入 next 方法,上来检查就不一致所以报错。
迭代器为什么不报错?
这说的很清楚了,就是迭代器会帮我们更新 expectModCount 的值,来使它与 modCount 保持一致,所以我们以后都要记得用迭代器的方法,才能保证不出错
至于上面为什么有时候报错,有时候就不报错,其实已经很简单了,跟着流程走一遍自然会发现。
总结:
迭代器的 next 方法会检查一致性,而自己集合的结构性操作只 modCount++,而不更新 expectModCount,所以会报错。