当前位置:首页 > 文章列表 > 文章 > java教程 > Java ArrayList 遍历删除完整流程:从 modCount 到 Iterator.remove 和 removeIf

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

来源:17golang原创 2026-06-15 17:44:59 0浏览 收藏

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删除
Java CompletableFuture 多接口聚合完整流程:并行调用、超时兜底和结果合并Java CompletableFuture 多接口聚合完整流程:并行调用、超时兜底和结果合并
上一篇
Java CompletableFuture 多接口聚合完整流程:并行调用、超时兜底和结果合并
下一篇
下一篇
暂无
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • MiMo Code - 小米大模型团队开源的新一代 AI 编程助手
    MiMo Code
    MiMo Code 是小米大模型团队开源的新一代 AI 编程助手,面向开发者提供代码理解、生成与辅助开发能力,适合作为 AI 编程工具收藏和体验。
    68次使用
  • TRAE Work - 字节跳动推出的 AI 原生工作台
    TRAE Work
    TRAE AI IDE | 国内首款 AI 原生集成开发环境,深度集成 Doubao-1.5-pro 与 DeepSeek 模型,支持中文自然语言一键生成完整代码框架,实时预览前端效果并智能修复 BUG。首创 Builder 模式实现需求到代码的自动化开发,兼容 Windows/macOS 系统,官网下载即用。
    96次使用
  • MeloLab - 一站式 AI 音乐生成与编辑平台
    MeloLab
    MeloLab 是一款 AI 音乐生成工具,可根据文本创意生成歌曲、人声、混音、分轨和背景音乐,适合创作者快速制作音乐素材。
    77次使用
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    8731次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    9145次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码