前言
- 最近突然想起刚毕业那会找工作时面试被问了个这样的问题。就是“使用增强for循环遍历ArrayList(List集合)时删除其中的元素是否会出现异常?”。说实话当时真把我愣住了,我当时的回答是:ArrayList内部使用的是Object数组,所以在增删时会自动挪动下标,而且对于数组而言长度是固定的,没有元素的位置会用null填充,虽然我没试过但我觉得不会抛异常。
- 现在想起这件事自己都觉得有些搞笑,哈哈,经验少也是没办法。现在回想既然面试官问得出这样的问题肯定是会抛出异常的,这个拿大腿都能想到,但是为什么呢?今天就这个问题来分析一下源码到底是怎么一回事。
- 文章分为两部分。首先是了解迭代器设计模式,然后再拿ArrayList作为例子进行源码分析。
迭代器模式
用途
- 遍历集合中的元素
- 优点是无序暴露集合内部信息,且迭代器可以为不同结构集合指定统一接口
- 缺点是每一个迭代器实现只对应一个特定结构的集合,也就是说如果集合类型繁多就增加迭代器的实现的个数
自定义迭代器
- 迭代器模式其实也很简单,主要角色有:自定义集合、迭代器接口、迭代器实现3个
- 以下例子并不严谨,只作为了解迭代器的参考
自定义集合
以数组作为内部存储结构(模仿ArrayList)
迭代器接口
迭代器实现
小结
- 迭代器的本质就是用来遍历某个时间点集合中的元素
- 在上面的例子中,MyListIterator 相当于在 MyList.iterator() 时copy了一份 MyList 的元素然后进行遍历
- 结合上面这个简单的例子可以得出结论,在迭代器迭代元素过程中,源集合序列是否允许被修改。上面例子我并没有做任何处理,所以在迭代器迭代元素过程中源集合序列是可以被修改。但这样就存在问题了,就是并发操作时迭代的元素序列可能会与源集合序列不一致。那为什么要引出这个问题呢?这是为了响应文章开头讨论的问题。即为什么增强for循环遍历List集合时删除元素会抛出异常,原因有以下两个:增强for循环使用迭代器实现(可以通过Debug观察)在JDK中集合的迭代器实现是不允许在迭代元素过程中源集合序列被修改的,即为了保证迭代元素序列和源元素序列的一致性。
ArrayList迭代器浅析
异常问题
可以看到在增强for循环遍历过程中如果删除一个元素会抛出 ConcurrentModificationException 。该异常的用意是并发修改是不允许的(说明源自 ConcurrentModificationException 类注释)。从异常中可以看到问题出自 ArrayList$Itr.next 方法,该方法中触发了 ArrayList$Itr.checkForComodification 导致了异常的发生
ArrayList$Itr (迭代器实现)源码
可以在源码中看到 next 方法中会先触发 checkForComodification 来检测源集合序列是否被修改,没有被修改才会继续操作,否则抛出异常。但我们想深一层,next 和 checkForComodification 两个方法其实都不是线程安全的,因此在多线程并发过程中并不能保证一致性。那么为什么需要 checkForComodification 呢?或者问 checkForComodification 的意义何在?其实这种操常被人叫做 fail-fast 机制,即快速失败。大概的用意是,如果不满足条件就没必要继续执行了,这时可以是跳过操作(比如continue和break),或者是抛出异常等等。这操作好比如我们在执行逻辑前会先判断传入的参数是否为null,如果为null则抛异常,道理是一样的。
增强for循环中删除元素是不是一定抛异常?
- 答案是否定的。原因是增强for循环本质是 Iterator 。而 Iterator 的操作步骤是先 hasNext 判断有没有下一个元素(判断的依据是源集合的size变量)。然后如果 hasNext 返回 true 才可以进行 next 操作。
- 所以什么情况下在增强for循环中删除元素不会抛出 ConcurrentModificationException 异常呢?答案就是,删除元素后(导致源集合序列的size减1)hasNext 正好返回false就不会抛出异常(删除的元素正好是倒数第2个)。因为返回false就不会触发 next 方法的 checkForComodification 操作。
- 例子如下:
喜欢的话点点关注~~~
作者:帧言