登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java ArrayList 遍历删除完整流程:从 modCount 到 Iterator.remove 和 removeIf

来源:17golang原创

时间:2026-06-15 17:44:59 410浏览 收藏

Java 里删除 List 元素,看起来是一个很小的动作:遍历列表,遇到符合条件的数据就删掉。但很多人第一次在线上遇到 ConcurrentModificationException,往往就是因为在增强 for 里直接调用了 list.remove

这篇文章不只给一个“用 Iterator 就好了”的答案,而是按完整流程拆开:先确定问题边界,再复现错误写法,接着看 modCount 为什么会变化,最后比较 Iterator.removeremoveIf 和复制新列表三种方案。看完以后,你可以按场景选择最稳的删除方式。

目录
  • 目标和边界:不是所有删除都要边遍历边删
  • 先说结论:遍历状态和集合结构修改要保持一致
  • 全流程总览:从增强 for 到异常抛出
  • 阶段 1:复现风险写法
  • 阶段 2:理解 modCount 和迭代器检查
  • 阶段 3:选择安全删除方案
  • 阶段 4:上线前做结果检查
  • 我的推荐流程
  • 容易踩坑
  • 速查表

目标和边界:不是所有删除都要边遍历边删

先把边界定清楚。本文讨论的是 ArrayList 在遍历过程中删除元素的常见问题,比如清理无效数据、过滤临时项、删除命中条件的对象。重点是单线程代码里也可能抛 ConcurrentModificationException,它不一定代表真的有多个线程同时改了列表。

如果你的需求只是“过滤出新列表”,并不需要修改原集合,那复制新列表更清晰。如果你的需求是“原集合必须就地删除”,那再考虑 Iterator.removeremoveIf。先分清这两类场景,后面代码会简单很多。

先说结论:遍历状态和集合结构修改要保持一致

增强 for 背后也是迭代器。当你在增强 for 循环里直接调用 list.remove,列表结构发生变化,但迭代器手里的预期修改次数没有同步更新。下一次迭代器检查时,就会发现状态不一致,于是抛出异常。

稳妥的路线是:需要边遍历边删除时,用迭代器自己的 remove;需要按条件批量删除时,用 removeIf;需要保留原数据时,生成一个过滤后的新列表。

全流程总览:从增强 for 到异常抛出

下面这张图把失败链路串起来:增强 for 开始遍历,循环中直接调用 list.remove,内部 modCount 变化,迭代器下一步检查时发现不一致,于是抛出异常。

Java ArrayList 增强 for 遍历删除导致 modCount 改变并抛出异常的流程图

阶段 目标 关键动作 检查点
阶段 1 复现问题 增强 for 中直接调用 list.remove 能稳定看到异常
阶段 2 理解原因 观察集合结构修改和迭代器状态 知道为什么不是线程问题
阶段 3 选择方案 Iterator.remove、removeIf 或新列表 代码表达符合业务意图
阶段 4 验证结果 检查删除前后数据和边界条件 不会漏删或误删

阶段 1:复现风险写法

先看最容易写出来的版本。这个代码想删除列表里的 B,但在增强 for 里直接改了原列表:

List names = new ArrayList(
        Arrays.asList("A", "B", "C", "D")
);

for (String name : names) {
    if ("B".equals(name)) {
        names.remove(name);
    }
}

这段代码的问题在于:遍历过程由迭代器维护,删除动作却绕开迭代器直接改了 List。代码短是短,但遍历状态和集合结构已经不一致。

这一阶段的检查点是:如果你看到异常栈里出现 ArrayList$ItrnextConcurrentModificationException 这些信息,就要优先检查是否在遍历中直接改了集合。

阶段 2:理解 modCount 和迭代器检查

ArrayList 内部会记录结构修改次数。新增、删除这类会改变列表结构的动作,会让这个计数变化。迭代器创建时,会记住当时的修改次数;后续每次取下一个元素时,再检查当前修改次数是否还匹配。

直接调用 list.remove 会让列表自己的修改次数增加,但迭代器不知道这次变化是“安全的”。于是下一次检查时,它发现两边数值对不上,就选择快速失败。

这就是为什么它叫 fail-fast:不是为了帮你自动修复数据,而是尽早告诉你“遍历过程中集合结构被不安全地改了”。

阶段 3:选择安全删除方案

修复不是只有一种写法。真正落地时,要先看业务意图:是边遍历边删除、按条件批量删除,还是保留原列表生成新列表。

Java List 安全删除元素的 Iterator.remove、removeIf 和复制新列表三种方案对比图

方案一:Iterator.remove

如果你需要在遍历过程中根据复杂逻辑删除当前元素,用迭代器自己的 remove 最直观:

Iterator it = names.iterator();
while (it.hasNext()) {
    String name = it.next();
    if (name.startsWith("A")) {
        it.remove();
    }
}

这里删除动作由迭代器完成,迭代器能同步自己的内部状态,所以不会出现前面的不一致问题。

方案二:removeIf

如果删除条件很清晰,推荐直接用 removeIf。它表达的业务含义更明确:删除所有满足条件的元素。

names.removeIf(name -> name.startsWith("A"));

这种写法适合批量过滤,例如删除所有临时项、无效项、空字符串。代码短,而且不需要手动维护迭代器。

方案三:复制新列表

如果原列表还要保留,比如后面要做审计、对比、回显,那么不要就地删除,直接生成新列表更安全:

List filtered = names.stream()
        .filter(name -> !name.startsWith("A"))
        .collect(Collectors.toList());

这种方式的检查点是:确认业务想要的是“过滤后的新结果”,而不是修改原集合。只要语义对了,后续维护的人也更容易理解。

阶段 4:上线前做结果检查

删除集合元素最怕两类问题:漏删和误删。上线前建议至少检查三组数据:

  • 没有命中条件:列表应该保持不变。
  • 全部命中条件:列表应该变空或新列表为空。
  • 连续多个命中:例如 A1、A2、A3 连在一起,确认不会跳过元素。

如果是业务对象列表,还要确认删除条件是否基于稳定字段,例如 ID、状态、类型,不要用可能变化的展示文案作为判断依据。

我的推荐流程

  1. 先判断是否必须修改原列表。
  2. 如果要保留原数据,优先复制新列表。
  3. 如果只是简单条件删除,优先用 removeIf
  4. 如果删除逻辑依赖多步判断,用 Iterator.remove
  5. 不要在增强 for 里直接调用 list.remove
  6. 补充连续命中、全部命中、没有命中的测试数据。

容易踩坑

  • 看到 ConcurrentModificationException 就以为是多线程:单线程增强 for 里直接删除也会触发。
  • 用普通 for 正向遍历并删除:删除后元素左移,很容易跳过下一个元素。
  • 为了不报错改成倒序删除但不写注释:短期能用,长期维护者不容易看出意图。
  • removeIf 里写复杂副作用:条件函数最好只表达判断,不要顺手改外部状态。
  • 忘记区分原列表和新列表:业务到底要不要保留原数据,必须先确认。

速查表

场景 推荐方式 检查点
遍历中按复杂条件删除当前元素 Iterator.remove 必须先 next,再 remove
按一个条件批量删除 removeIf 条件函数保持清晰
保留原列表,产出过滤结果 复制新列表 原列表不应被修改
少量元素按下标删除 倒序下标删除 避免元素左移导致跳过
增强 for 里直接删除 不推荐 容易触发异常

总结

ArrayList 遍历删除的问题,本质不是“Java 集合不好用”,而是遍历状态和结构修改没有走同一条路径。增强 for 负责遍历,list.remove 负责直接改集合,两边状态对不上,就会快速失败。

落地时可以记住一个简单判断:要在当前遍历里删,用 Iterator.remove;要按条件批量删,用 removeIf;要保留原数据,用新列表。把选择标准写清楚,比记住某个零散技巧更稳。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>