先说结论:checkForComodification 方法,在 AbstractList 的迭代器内部,几乎每次你调用几个核心操作——next()、remove()、previous()、set() 或 add()——它都会预先执行一次。这就像每次出门前都要检查门锁,不是只查一次,而是每次操作都校验。

哪些方法会触发 checkForComodification?
这个检查并不仅限于遍历开始或结束时才出现,而是嵌入在迭代器核心操作的入口处。以最常见的 ArrayList.Itr(它继承自 AbstractList.Itr)为例,这些方法都是“前置校验”的忠实执行者:
next():在获取下一个元素之前,立即进行校验。remove():在删除上一个返回的元素之前,同样需要校验。注意,这里指的是迭代器自身的remove方法,而非集合的remove。previous()和next():在ListIterator中,这两个方法也都在入口处执行校验。set()和add():这两个方法只有ListIterator支持,但同样严格校验,一个都不放过。
为什么是“每次”而不是“仅首次”?
这个问题问得很好。原因很简单:结构性修改可能发生在任意两次迭代步骤之间。举个例子,线程 A 刚遍历到第 3 个元素,线程 B 突然调用了 list.add(),导致 modCount 发生变化。这时线程 A 如果继续调用 next(),就需要立刻发现这个不一致。如果只在首次检查,那后续可能出现的状态错乱、跳过或重复元素,就完全无法控制了。因此,即时校验的目的就是确保每一步操作都基于“当前视图的一致性”——每一步都踩实了再走,这正是 Java fail-fast 机制的核心设计。
有没有不触发 checkForComodification 的情况?
当然有。以下场景就不会触发这个检查:
- 仅仅读取元素,比如直接调用
get(int)方法,不涉及迭代器,自然不触发。 - 使用
Enumeration,比如Vector.elements(),它本身就不是 fail-fast 的,所以也不会检查。 - 迭代器自身的合法结构修改:比如调用
iterator.remove(),它会同步更新expectedModCount,所以后续的next()自然不会报错。
关键细节:校验发生在“操作前”还是“操作后”?
这个问题很关键。标准实现,比如 ArrayList.Itr.next(),是先校验,再执行业务逻辑。有些 JDK 版本还会在 finally 块里再校验一次,防止操作中途被中断导致状态不一致。但主校验点始终在方法入口。这种设计保证了“失败即刻可见”,这恰恰是 fail-fast 的本质——不掩盖问题,不延迟暴露。值得警惕的是,这种设计虽然严谨,但也意味着如果你在迭代过程中不小心修改了集合,程序会立刻报错,而不是等到最后才出问题,这也算是一种“早发现、早治疗”的思路。
