集合遍历删除元素为什么推荐使用Iterator?

文章导读
之前排查过一起线上问题:业务日志里频繁出现 ConcurrentModificationException。我首先让运维把报错现场和堆栈捞出来,确认是哪个接口、哪个集合在遍历时被修改了。从堆栈看,是在一个 for-each 循环里直接调用了 list.remove() 方法。当时第一反应就是“又有人用 for-each 删除元素了”,因为这个问题在团队里反复出现过。
📋 目录
  1. 先确认现象:异常类型与堆栈定位
  2. 容易误判的地方:为什么 for-each 和普通 for 循环都不稳妥
  3. 建议的处理顺序:使用 Iterator 的正确方式
  4. 验证方法:单元测试与边界检查
  5. 回滚和风险:紧急止血与后续修复
A A

之前排查过一起线上问题:业务日志里频繁出现 ConcurrentModificationException。我首先让运维把报错现场和堆栈捞出来,确认是哪个接口、哪个集合在遍历时被修改了。从堆栈看,是在一个 for-each 循环里直接调用了 list.remove() 方法。当时第一反应就是“又有人用 for-each 删除元素了”,因为这个问题在团队里反复出现过。

先确认现象:异常类型与堆栈定位

出现 ConcurrentModificationException 时,不要只盯着异常名。我会先去查堆栈顶部的代码行,找到具体是哪个集合、哪个循环。常见的场景是 ArrayList、HashSet 或 HashMap 在增强 for 循环中执行了 remove 操作。堆栈中通常会有一行类似 java.util.ArrayList$Itr.checkForComodification 的调用,说明迭代器检测到了并发修改。

如果堆栈里没有明确指向自己的业务代码,还要确认是不是集合被多线程并发修改了。单线程下,for-each 删除是主要原因。先确认是单线程还是多线程环境,这对后续修复方式有直接影响。

容易误判的地方:为什么 for-each 和普通 for 循环都不稳妥

for-each 循环本质上是使用 Iterator 进行遍历,但循环体内直接调用集合的 remove 方法会修改集合的 modCount,而迭代器内部的 expectedModCount 没有同步更新,导致下次检查时抛出异常。很多人觉得“我用 for 循环加索引删除,只要处理好 i-- 就可以了”,但索引删除在连续删除多个相邻元素时很容易漏删或越界。比如从 0 开始遍历,删除第 i 个元素后,后面的元素会前移,继续 i++ 就会跳过下一个元素。虽然有办法用倒序遍历加 i-- 来规避,但这种写法不够直观,而且如果集合是 LinkedList 这种随机访问性能差的实现,索引删除效率很低。

集合遍历删除元素为什么推荐使用Iterator?

还有一种误判是认为“只有循环内执行 remove 才会异常”,其实在迭代过程中调用集合的 add 或 clear 也会触发同样的问题。所以判断条件不是 remove 这个动作,而是在迭代过程中任何结构性修改(除了迭代器自己的修改方法)。

建议的处理顺序:使用 Iterator 的正确方式

正确的做法是使用 Iterator 的 remove 方法。先通过 collection.iterator() 取得迭代器,然后在 while 循环中调用 iterator.next() 获取元素,满足条件时调用 iterator.remove()。因为 Iterator 的 remove 会在删除元素后同步更新 expectedModCount,而且它使用的是集合内部的删除方法,但通过迭代器来维护一致性。具体步骤:

  • 在循环外获取 Iterator 对象。
  • while (iterator.hasNext()) 遍历。
  • 取元素:iterator.next()
  • 判断条件。
  • 删除:iterator.remove()

如果 JDK 版本在 8 以上,也可以使用 Collection.removeIf() 方法,传一个 Predicate lambda,内部也是基于 Iterator 实现的。但注意 removeIf 适用于所有标准集合,且无需显式创建迭代器,写法更简洁。如果集合很大且删除条件复杂,建议先用 removeIf 测试性能。

还有一个风险点:如果是在多线程环境下遍历并删除,即使使用 Iterator 的 remove,也可能出现线程安全问题。此时需要加锁或用并发集合如 ConcurrentHashMap(但它不支持在迭代时添加,不过可以用其提供的批量方法)。建议先用单线程验证,再考虑并发场景。

集合遍历删除元素为什么推荐使用Iterator?

验证方法:单元测试与边界检查

写一个简单的单元测试,覆盖以下几个场景:空集合、只有一个元素(删除它)、相邻元素连续删除、尾部元素删除。测试时断言集合剩余元素数量和顺序是否符合预期。使用 assertIterableEquals 或手动检查列表内容。例如对于 ArrayList,可以验证删除后集合的 size 和元素是否准确。

还有一个验证点:确认删除后循环是否完整遍历了所有元素。可以通过在删除前后打印集合内容来观察。如果使用 for-each 删除,你根本跑不到单元测试那步就会抛出异常,所以验证本身就能暴露问题。

如果代码已经部署到预发环境,建议开启调试日志,输出进入循环的次数和删除的元素,对比线上正常流量数据看是否有漏删或重复删除。

集合遍历删除元素为什么推荐使用Iterator?

回滚和风险:紧急止血与后续修复

如果线上已经因为 for-each 删除导致异常影响了业务,最快的止血方案是把删除逻辑临时改为“先收集要删除的元素,遍历结束后统一删除”。比如新建一个临时 List 存需要删除的索引或元素,遍历完成后调用 removeAll 或循环删除(注意索引顺序倒序)。这样做可以立刻恢复接口正常,但注意 removeAll 的性能:对于大集合可能较慢,并且如果元素可重复,removeAll 会删除所有匹配项,可能导致误删。

更稳妥的紧急修复是直接回滚上一个变更。如果变更集较小,可以直接还原这部分代码,等彻底测试 Iterator 版本后再发布。回滚后记得在监控里确认异常消失,同时观察业务数据是否有残留。

后续维护建议:在团队代码规范中明确规定“遍历删除必须使用 Iterator 或 removeIf”,并且把常见反例(for-each 删除、索引删除未处理偏移)写入 Code Review 检查清单。如果有自动化静态检查工具,可以配置禁止在 for-each 循环体内调用集合的 remove 方法。