集合框架的暗坑:ArrayList遍历时删除元素的灾难

前言

请添加图片描述

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家:https://www.captainbed.cn/z
在这里插入图片描述
请添加图片描述


1. 错误场景复现

场景1:增强型for循环删除元素

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));

// 尝试删除包含 "B" 的元素
for (String s : list) {           // 触发 ConcurrentModificationException
    if ("B".equals(s)) {
        list.remove(s);          // 直接操作原集合
    }
}

增强型for循环底层使用迭代器,直接调用List.remove()会破坏迭代器状态。


场景2:索引遍历的遗漏问题

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

// 删除所有偶数
for (int i = 0; i < numbers.size(); i++) {
    if (numbers.get(i) % 2 == 0) {
        numbers.remove(i);      // 删除后未调整索引,导致跳过元素
    }
}

// 最终结果:[1, 3, 5]?错误!实际输出:[1, 3, 4](元素4被跳过)

正向遍历时删除元素会导致后续元素前移,索引未回溯引发逻辑错误。


场景3:多线程并发修改

List<String> dataList = new ArrayList<>(Arrays.asList("data1", "data2"));

// 线程1:遍历集合
new Thread(() -> {
    for (String s : dataList) {  // 可能触发ConcurrentModificationException
        System.out.println(s);
        Thread.sleep(1000);      // 模拟耗时操作
    }
}).start();

// 线程2:修改集合
new Thread(() -> {
    dataList.add("data3");       // 修改集合结构
}).start();

ArrayList非线程安全,多线程同时读写时可能抛出异常甚至导致数据损坏。


2. 原理解析

ConcurrentModificationException 的触发条件

// ArrayList.Itr 迭代器实现核心逻辑
int expectedModCount = modCount;  // modCount记录集合结构修改次数

public boolean hasNext() {
    return cursor != size;
}

public E next() {
    checkForComodification();  // 关键检查:比较expectedModCount与modCount
    // ... 其他逻辑
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
  • modCount:集合结构变化(增/删)时自增的计数器
  • expectedModCount:迭代器创建时记录的modCount快照
  • 检测机制:任何遍历过程中修改集合的操作都会导致两者不一致

为什么允许通过迭代器删除?

// ArrayList.Itr.remove() 的实现
public void remove() {
    // ... 前置检查
    ArrayList.this.remove(lastRet);  // 调用集合的remove方法
    expectedModCount = modCount;     // 手动同步计数器
    // ... 其他逻辑
}

迭代器的remove()方法会在操作后同步expectedModCount,避免抛出异常。


3. 正确解决方案

方案1:显式使用迭代器删除(单线程场景)

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    if ("B".equals(s)) {
        iterator.remove();  // 安全删除当前元素
    }
}
优势:
  • 通过迭代器内部维护状态
  • 支持动态集合修改
限制:
  • 不适用于并发场景
  • 只能删除当前遍历元素

方案2:使用Java 8的removeIf方法

list.removeIf(s -> "B".equals(s));  // 底层使用迭代器批量删除

// 等价于:
list = list.stream()
          .filter(s -> !"B".equals(s))
          .collect(Collectors.toList());
优势:
  • 代码简洁,内部处理并发修改检查
  • 支持Lambda表达式编写条件
性能注意:
  • 需要创建新集合对象(Stream方式)

方案3:反向遍历删除(适用于索引遍历)

for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).contains("B")) {
        list.remove(i);  // 从后往前删除不会影响未遍历的索引
    }
}
适用场景:
  • 需要基于索引的复杂判断逻辑
  • 无法使用迭代器的场景

方案4:并发安全集合(多线程场景)

// 使用CopyOnWriteArrayList(读多写少场景)
List<String> safeList = new CopyOnWriteArrayList<>(list);

for (String s : safeList) {  // 迭代器持有旧数组快照
    if ("B".equals(s)) {
        safeList.remove(s);  // 操作新数组,不影响遍历
    }
}

// 或使用Collections.synchronizedList + 同步块
synchronized (syncList) {
    Iterator<String> it = syncList.iterator();
    while (it.hasNext()) {
        String s = it.next();
        if ("B".equals(s)) {
            it.remove();
        }
    }
}
选型建议:
  • CopyOnWriteArrayList:适合遍历频繁、修改少的场景(有内存开销)
  • synchronizedList:适合写操作较多的场景(需手动同步)

4. 工具与最佳实践

静态分析工具配置

  1. IntelliJ IDEA检测:

    • 自动标记直接修改集合的代码
    • 提示替换为iterator.remove()
  2. SonarQube规则:

    • "java:S2272" - 检测非迭代器删除操作
    • "java:S2114" - 检查可能无效的集合批处理

Code Review检查清单

检查项正确做法
遍历时是否直接操作原集合?必须通过迭代器或并发安全集合修改
多线程环境是否使用线程安全集合?选择CopyOnWriteArrayList或ConcurrentHashMap
是否误用subList()?注意subList与原集合的修改相互影响
大数据量删除是否高效?优先使用removeIf()或批量操作API

5. 真实案例

某社交平台的消息队列处理服务中,开发人员使用以下代码清理过期消息:

for (Message msg : messageList) {
    if (msg.isExpired()) {
        messageList.remove(msg);  // 直接调用remove
    }
}

后果:

  • 高峰时段触发ConcurrentModificationException,消息堆积导致服务宕机
  • 累计丢失3万条未处理消息

修复方案:

  1. 替换为线程安全的CopyOnWriteArrayList
  2. 使用并行流处理加速清理:
    messageList.parallelStream()
               .filter(msg -> !msg.isExpired())
               .collect(Collectors.toList());
    

总结

  • 禁止在遍历中直接操作原集合:通过迭代器或专用API修改
  • 线程安全是分层级的:单线程用迭代器,多线程用并发容器
  • 善用Java 8+新API:removeIf()/Stream 简化代码且安全
  • 逆向思维解决问题:反向遍历可规避索引错位问题

下期预告:《String拼接的代价:为何StringBuilder是救星?》——从JVM内存模型解析字符串操作的性能黑洞,揭秘JDK隐藏的优化技巧。

联系作者

职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集
在这里插入图片描述
在这里插入图片描述

Java集合容器深度解析:从源码原理到面试实战选型指南 在Java开发中,集合框架是日常编码与面试考察的核心知识模块。无论是ArrayList的扩容机制、HashMap的红黑树优化,还是并发容器的锁粒度演进,其本质都是对数据结构设计与性能取舍的工程实践。理解集合框架从接口抽象到具体实现的层次关系,掌握快速失败机制、哈希扰动函数、负载因子等关键原理,有助于开发者做出合理的容器选型。例如,在涉及大量元素存储时预估容量避免频繁扩容,在并发读写场景下根据读写比例选择ConcurrentHashMap或CopyOnWriteArrayList。集合框架的设计思想还渗透至工 阅读详情

相关推荐

深入浅出Java ArrayList:动态数组的灵活之道

ArrayList作为Java集合框架的基石,其设计体现了空间换时间的经典思想。通过理解它的动态扩容机制、时间复杂度特性以及线程安全问题,我们能在实际开发中做出更明智的选择。就像选择交通工具:ArrayList是"空间宽敞但需要定期加油的SUV",LinkedList是"穿街走巷灵活的摩托车",而Vector则是"笨重但结实的装甲车"。理解它们的特性,才能为不同场景选择最合适的工具。记住这个黄金法则数据量有多大?主要操作是什么?(查询/增删)是否需要线程安全?

weixin_49583029的博客 734

智能车竞赛新手必看:用AD21从零画一块英飞凌TC264核心板(附开源PCB文件)

本文详细介绍了使用Altium Designer 21(AD21)从零开始设计英飞凌TC264核心板的完整流程,涵盖原理图绘制、PCB布线到最终DRC检查的全过程。针对智能车竞赛新手,提供了实用的硬件设计技巧和开源PCB文件,帮助快速掌握TC264核心板开发。

weixin_27017211的博客 247

自旋玻璃(spin glass)、自旋冰(spin ice)和量子自旋液体(quantum spin liquid)(之二)

具有相同电荷的电子相互排斥。结果,为了在极其拥挤的环境中相互超越,他们不得不改变自己的行为。剑桥大学和英国伯明翰大学于 2009 年 7 月发表的研究表明,电子可以通过量子隧穿从金属表面跳到相近位置的量子线上,这样做后,将分离成两个准粒子,命名为的自旋子和全子。Van den Brink、Khomskii 和 Sawatzky 在 1997-1998 年对轨道子进行了理论上的预测。在 2011 年 9 月发给出版商的论文中报告了它作为一个单独的准粒子的实验观察结果。

qq_32515081的博客 1万+

考虑风和浪的影响的旋筒帆船航线优化方法研究

针对旋筒帆船进行航线优化的实现方法,包括升阻力系数的数值模拟、升阻力系数的多元非线性回归建模、旋筒帆船6DOF摇荡运动建模分析以及考虑风、浪影响的旋筒帆船的航线优化研究。

m0_63225885的博客 1165

DeepSeek与豆包无缝对接:5分钟搞定智能周报自动化(附完整代码)

本文详细介绍了如何利用DeepSeek与豆包API,快速搭建一个智能周报自动化系统。通过整合豆包平台的工作流数据,并调用DeepSeek大模型进行深度分析与内容生成,该系统能在5分钟内自动抓取、分析多源信息,并生成结构清晰、富有洞察力的团队周报,显著提升工作效率。

weixin_29029995的博客 494

ReportStudio入门教程(四十一) - 固定报表列宽

我们继续上一篇博客,让他看起来

什么是江湖? 3518

手把手配置:用Synopsys VDK为英飞凌TC4x系列MCU做虚拟原型开发

本文详细介绍了如何使用Synopsys VDK为英飞凌TC4x系列MCU进行虚拟原型开发,涵盖开发环境搭建、关键外设虚拟化配置、AUTOSAR栈集成及虚拟调试与性能调优全流程。通过虚拟原型技术,开发者可提前6-12个月启动软件开发,显著提升汽车电子项目的开发效率。

weixin_30606461的博客 292

Python调用CANoe(1)(启动与停止,变量和信号读写)

CANoe因为其封闭性(Vector独有),开发的时候,有很多局限性;Python 因为其开源,容易上手,功能强大,那么Python 和 CANoe结合会产生怎样的火花呢?

蚂蚁小兵 4万+

【AI智能体】Codex 高级进阶使用技巧项目实战操作详解

Codex 高级进阶使用技巧项目实战操作详解

congge 1万+

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

上一篇: 常见的空指针异常(NullPointerException)及预防方法
下一篇: Spring框架下的依赖注入坑:循环依赖与作用域混淆
雪碧有白泡泡
雪碧有白泡泡 优质创作者: 编程框架技术领域 优质创作者: 编程框架技术领域
博客等级 码龄5年 1万+粉丝 44原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雪碧有白泡泡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值