不能在for-each循环中直接删除元素,因其导致modCount与expectedModCount失步,触发ConcurrentModificationException或漏删元素,属单线程下确定性逻辑缺陷。

不能在 for-each 循环中直接删除元素,不是因为“语法不允许”,而是它天然存在逻辑缺陷——遍历与修改职责错配。for-each 是语法糖,背后是 Iterator;而你在循环体里调用的是 list.remove(),这个操作由集合自己执行,和迭代器完全无关。这就导致两个关键变量失步:
modCount 和 expectedModCount 脱节
ArrayList 内部有个 modCount,每次 add/remove 都自增;Iterator 创建时会把当时的 modCount 记为 expectedModCount。后续每次调用 next() 前,都会执行 checkForComodification() 比较二者。一旦不等,立刻抛 ConcurrentModificationException。
- 删第一个元素:下一次
next()就触发检查 → 立即报错 - 删倒数第二个元素:可能侥幸走到末尾才检查,或甚至没走到检查点 → 表面不报错,但最后一个元素被跳过
遍历指针“看不见”结构变化
for-each 的底层 while 循环依赖 hasNext() 判断是否继续,而该方法只看当前 cursor 是否等于 size。当你删掉一个元素,size 变小、后续元素前移,但 cursor 仍按原节奏递增,结果就是:
- 下一个元素被“跨过去”,漏处理
- 重复元素(如多个 "aaa")几乎必然删不全
- 行为高度依赖删除位置和集合当前状态,不可预测
这不是并发问题,是单线程下的确定性陷阱
很多人误以为 CME 是多线程专属,其实不然。哪怕只有主线程,只要“用迭代器遍历” + “用集合方法修改”,就触发 fail-fast。这是 Java 集合框架的主动防御设计:宁可中断,也不让结果出错。
- fail-fast 不是 bug,是 feature —— 它暴露了你代码里隐含的逻辑矛盾
- 所谓“有时不报错”,恰恰更危险:掩盖了数据遗漏,上线后对账偏差、订单丢失都可能由此引发
识别这个缺陷的关键,不是看有没有异常,而是看你的删除动作是否发生在 for-each 循环体内。只要出现 list.remove(...) 或 set.remove(...) 这类调用,就已踩中红线。










