集合迭代失效本质是迭代器与集合间modCount版本号契约被破坏,单线程下for-each中直接remove也会触发ConcurrentModificationException;安全删除仅三种:Iterator.remove()、removeIf()、先收集后批量删;CopyOnWriteArrayList不适用通用场景。

理解集合迭代失效,关键不是记住“不能在 for-each 里删元素”,而是看清它背后是一套状态契约:迭代器和集合必须共享同一份结构版本号。只要这个契约被打破,异常就不是 bug,而是系统在喊你“逻辑出问题了”。
失效不是崩溃,是契约被破坏的明确提醒
Java 所有主流集合(ArrayList、HashMap、HashSet 等)都内置一个 modCount 字段,它只记录结构性修改次数(add、remove、clear),不关心内容改没改。当你调用 iterator(),迭代器立刻把当时的 modCount 复制一份,存成自己的 expectedModCount。之后每次 next() 或 hasNext() 前,都会比对这两个值。
- 相等 → 集合结构没被意外动过,继续走
- 不等 → 契约失效,立刻抛
ConcurrentModificationException
注意:这个机制单线程下同样生效。for-each 循环本质就是用 Iterator,你在里面写 list.remove(x),等于一边读一边偷偷改版本号,迭代器一校验就炸——这不是多线程专属问题,而是遍历语义被违反。
真正安全的删除,只有三种可靠路径
别再纠结“能不能删”,直接锁定这三种已被验证无风险的方式:
-
Iterator 自带 remove():必须紧跟
next()调用,且只能调一次。它内部会同步更新 expectedModCount,让契约继续有效 -
Java 8+ 的 removeIf():
list.removeIf(s -> s.startsWith("A")),底层就是用迭代器安全删除,代码简洁且语义清晰 -
先收集、后批量删:遍历中把要删的元素加进临时集合,遍历完再调
list.removeAll(temp)。适合需要复杂判断或跨多个条件的场景
别误用 CopyOnWriteArrayList 当“万能解药”
它确实不报 ConcurrentModificationException,但原因不是“绕过了检查”,而是压根不检查:每次 iterator() 都返回当前数组的不可变快照,所有写操作(add/remove)都会新建数组并复制全部元素。这意味着:
你是 TypeScript 项目架构专家,专注于脚手架搭建生产就绪的 Node.js 和前端应用。生成包含现代工具链(pnpm、Vite、Next.js)、类型
- 迭代器永远看不到新添加的元素(只读旧快照)
- 写操作开销是 O(n),50 万条数据每次 add 都要复制 50 万引用
- 如果高频创建迭代器 + 长生命周期持有(比如放进异步任务),极易引发内存堆积甚至 OOM
它只适合读极多、写极少、且能接受最终一致性的场景,比如监听器列表、配置白名单,不是通用替代方案。
面试或查 Bug 时,一眼定位根源的方法
遇到这个异常,别急着改代码,先问三个问题:
- 异常栈里报错行,是在 for-each 循环体里?→ 十有八九是直接调了集合的 remove/add
- 是不是多个线程共用同一个 ArrayList/HashMap?→ 即使加了 synchronized,也要确认锁范围是否覆盖全部读写操作
- 有没有在 Stream 操作里又去修改原集合?→
list.stream().forEach(s -> list.remove(s))同样触发 fail-fast
答对这四层,你就不是在“防报错”,而是在构建可预测、易维护的集合操作逻辑。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










