Iterator 与 fail-fast,属于那种“越熟悉越容易翻车”的考点:很多人自信满满地答完“迭代器用来遍历”,面试官一句“在 for-each 里 remove 会怎样”,台面上就有人当场写错。这题考的从来不是名词,而是能不能把“异常为什么抛、怎么避免、它到底保不保证并发安全”这三层因果讲清。
先把结论放在前面:fail-fast 是集合在迭代期间检测到“结构被意外修改”时,尽快抛 ConcurrentModificationException 的一种尽力而为的早期预警,不是并发安全保证;无锁并发下它可能不抛、但数据已经损坏。删除元素要走迭代器的 remove(),而不是集合的 remove()。 这题要答好,得先清楚 modCount 机制,再讲清“什么算结构修改、异常何时抛、并发该用什么”。
机制拆解
讲清 modCount 与 expectedModCount 的配合。ArrayList、HashMap 这类集合维护一个 modCount 计数器,每次结构性修改(增、删、清空等)都自增;迭代器创建时会记下 expectedModCount = modCount,每次 next / remove 前比对两者,不一致就抛异常。 注意“结构性修改”的界定:ArrayList 的 add/remove 算,但某些实现里单纯 set 不算;HashMap 的 put/remove 算,但若只改已存在 key 的 value,视实现可能不计数。理解这点才能解释“为什么有时改了也不抛”。
这些坑的正确绕法
最常见的坑是在增强 for 循环里调用集合的 remove。增强 for 本质是迭代器遍历,循环体内 list.remove(obj) 改的是集合而非通过迭代器,下一次 next 检测到 modCount 不一致立刻抛 ConcurrentModificationException;即使侥幸没抛(比如删倒数第二个元素后循环正好结束),也是踩在不确定行为上。 正确做法是用迭代器的 it.remove()。
其次是下标 for 循环删除却忘了回退索引。list.remove(i) 后后面的元素整体前移,若 i 不自减,就会跳过被前移过来的那个元素,导致“删不干净”。这是批量按条件删除时极常见的漏删 bug。
还有一个更隐蔽的坑:依赖 fail-fast 做并发防护。fail-fast 只在单线程“一边遍历一边改”的场景给出尽早报错,多线程并发修改时它可能根本来不及抛、或只在某些线程抛,数据却已经处于不一致状态。真正并发场景必须用并发容器或外部锁。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// Iterator 与 fail-fast
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
Integer v = it.next();
if (v < 0) it.remove(); // 安全删除:同步 modCount
}
// list.remove 在迭代中触发 ConcurrentModificationException
// modCount 机制:结构性修改次数校验
// 并发场景用 CopyOnWriteArrayList(迭代快照,不抛 CME)
这段代码值得盯三处:第一处,it.remove() 在删除的同时同步了 modCount,不会触发异常;第二处,直接用 list.remove 破坏的是集合的 modCount 而非迭代器记录的 expectedModCount,于是 next 抛异常;第三处,CopyOnWriteArrayList 走快照迭代,适合读多写少的并发读,但代价是迭代期间看不到最新写入。面试讲到这一层,基本就稳了。
这题在面试里怎么问、怎么答
“请简单介绍一下 Iterator 与 fail-fast,它在 Android 开发中起什么作用?”一句话:迭代器提供统一遍历接口,fail-fast 在遍历被意外修改时尽早报错。在 Android 里,列表数据常在后台线程变更、主线程遍历渲染,理解它能避免“遍历中改数据”导致的崩溃。 如果要把约定沉淀成团队规范,建议加一条:遍历中删除一律走迭代器 remove,不在 for-each 里调集合 remove。
“Iterator 与 fail-fast 的底层原理是什么?能不能详细说一下?”从 modCount 与 expectedModCount 的对比讲起,说明异常是“检测不一致后尽早抛出”而非“阻止修改”;再界定什么是结构性修改。机制别空讲,配核心代码最稳。
“在使用时遇到过什么问题?”客观地说实案例:某列表在遍历中按条件删除,用了下标 for 循环却没回退索引,导致部分元素漏删、界面出现重复脏数据;改成迭代器 remove 后行为正确。量化的修复效果最加分。
“和相关的替代方案相比,有什么优劣?”与手动下标遍历比,迭代器 remove 安全且语义清晰;与 CopyOnWriteArrayList 比,后者适合并发读、但写时复制有内存与实时性代价;与并发容器的快照迭代比,普通集合的 fail-fast 只是单线程防护。选型时要把“fail-fast 不是并发安全保证”这类代价摆到台面上,再决定是否引入。
再补一个工程上值得讲清的点:fail-fast 为什么叫“尽力而为”。它只在迭代器的 next / remove 等方法的入口做 modCount 校验,校验是“可能检测到、也可能漏掉”的——比如两个线程并发改,恰好在某一刻 modCount 被改但当前线程的 next 已经越过校验点,异常就不抛,可数据已经错。所以它定位是“帮助尽快暴露编程错误”,而不是“并发正确性保障”。 另一个常被追问的是:那 List 遍历删除到底该用什么才稳?单线程场景用迭代器 remove 即可;若是多线程、且遍历与修改可能并发,应当换用 CopyOnWriteArrayList(读无锁、写复制)或加锁,而不是指望 fail-fast 来兜底。把“单线程防错”和“多线程防并发”这两件事分清,才不会被面试官追问到语塞。
再补一个把这条线索串起来的观察:fail-fast 其实是 Java 集合设计里少见的“主动报错”选择——多数 API 会选择静默容忍错误行为,fail-fast 则明确把代价摆在前面,用一次异常换掉一整类难以排查的数据错乱。 这也是它和 C++ 迭代器失效、Go 的 map 迭代顺序不定这类设计差异最值得讲的地方:三者面对“迭代期间修改容器”给出了三种态度,Java 选了最激进的一种,而 JDK 8 又在并发容器上把它放宽成弱一致,说明这条规则也在演进。把这种“设计取舍 + 历史演进”的视角讲出来,比单纯背下 modCount 机制高一个层次,面试官往往就是在这一层给出评价。
再补一个和本篇相邻、面试官常顺势带到的点:modCount 的具体类型与设计意图。ArrayList 用的是 int,AbstractList 里则定义为 protected transient int modCount,transient 意味着它不参与序列化——因为它只是运行期的结构变更计数,没有跨进程恢复的必要。 这个字段从设计之初就是给迭代器做校验用的,属于典型的"为机制而存在的状态"。另一个值得提的边界是 subList:它返回的视图与原列表共享同一个 modCount,因此通过视图对原列表做的结构性修改,会让另一侧正在进行的迭代立刻失效。 这个细节能解释一类真实故障——业务把 subList 结果交给另一个线程遍历,而主线程同时在刷新源列表,于是抛出 ConcurrentModificationException,而两边代码单看都没问题。
给正在准备面试的你
把 Iterator 与 fail-fast 画成“modCount vs expectedModCount”的对比小图,标出校验时机与异常触发点。面试中关于 Iterator 与 fail-fast 的问题,关键在于能够从原理、应用、踩坑三个层面给出有深度的回答。
再补工程案例与踩坑——应用落点是把一个 for-each 内 list.remove 的崩溃点改成迭代器 remove,并对比 CopyOnWriteArrayList 在并发读场景下的取舍。
复习时别孤立刷题:反射基础——相邻考点常被一起问,边界提前划清楚。
划两句重点:fail-fast 是 modCount 不一致的尽早预警,不是并发安全保证;遍历中删除走迭代器 remove,并发场景用并发容器或加锁。
下一篇聊反射基础:Class 对象与运行时类型信息——沿着今天这条主线继续往前走。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:TreeMap-与排序:Comparable-与-Comparator
下一篇预告:反射基础:Class-对象与运行时类型信息
有任何问题欢迎在评论区留言交流。