当前位置:首页 > 文章列表 > 文章 > 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 多接口聚合完整流程:并行调用、超时兜底和结果合并
前端 WebSocket 断线重连完整流程:心跳检测、退避重试和消息补发
下一篇
前端 WebSocket 断线重连完整流程:心跳检测、退避重试和消息补发
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    4715次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4319次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4268次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    4495次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    4451次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码