不能在 foreach 中直接调用 list.remove(),根本原因是 java 集合的快速失败机制通过 modcount 与 expectedmodcount 校验一致性,单线程下手动 remove 也会导致二者不等而抛 concurrentmodificationexception。

不能在 foreach 中直接调用 list.remove(),根本原因不是“并发”或“多线程”,而是 Java 集合的快速失败(fail-fast)机制在起作用——它专为检测遍历中被意外修改而设计,哪怕单线程也会立即报错。
底层靠 modCount 计数器校验一致性
ArrayList、HashMap 等集合内部维护一个 modCount(修改计数器),每次 add/remove 结构性操作都会自增。而 foreach 底层使用 Iterator,创建时会把当时的 modCount 值存为 expectedModCount。之后每次调用 next() 或 hasNext(),都会检查二者是否相等:
- 相等 → 继续遍历
- 不相等(比如你手动调了
list.remove(),modCount变了但expectedModCount没变)→ 立即抛ConcurrentModificationException
foreach 本质是迭代器语法糖,不是“普通循环”
写成这样:
for (String s : list) {
if (s.equals("test")) list.remove(s);
}
编译后等价于:
Iterator<string> it = list.iterator();
while (it.hasNext()) {
String s = it.next(); // 这里就会检查 modCount!
if (s.equals("test")) list.remove(s); // 修改了 modCount
}</string>
注意:出问题的不是 remove() 本身,而是它发生在 it.next() 的两次调用之间,破坏了迭代器对集合状态的预期。
为什么 Iterator.remove() 就可以?
因为 iterator.remove() 是迭代器自己提供的方法,它会在删除的同时同步更新自己的 expectedModCount,保持与集合一致。相当于:“我删的,我负责记账”。
- 安全写法:
iterator.remove() - 安全写法:
list.removeIf(x -> x.equals("test"))(Java 8+) - 不安全写法:
list.remove(x)、list.add(x)、list.clear()等任何结构性修改
别误用 CopyOnWriteArrayList 当“万能解”
CopyOnWriteArrayList 确实不会抛 CME,但它通过“读时复制”实现,每次写操作都新建数组,适合读远多于写的场景(如监听器列表)。它的迭代器是快照,遍历时删元素对当前迭代器不可见,也不影响正在遍历的数据——但这不是“修复问题”,而是换了一种语义:你删了,但这次循环看不到。
日常业务逻辑中,若需边遍历边筛选,优先考虑 removeIf() 或流式过滤重建列表,语义更清晰、性能也更可控。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











