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

本文探讨了如何结合使用Apollo和微服务架构来构建可扩展的自动驾驶系统。介绍了微服务架构的原理、挑战及解决方案,强调了在微服务中使用Apollo进行数据交互和管理的重要性,以及如何处理数据一致性和事务。通过这种方式,可以构建出适应城市交通需求、具有高可扩展性和弹性的自动驾驶应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

请添加图片描述

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家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+新APIremoveIf()/Stream 简化代码且安全
  • 逆向思维解决问题:反向遍历可规避索引错位问题

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

联系作者

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

雪碧有白泡泡

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

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

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

打赏作者

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

抵扣说明:

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

余额充值