Java中for循环内修改集合的常见陷阱与最佳实践

 更新时间:2025年06月16日 08:24:16   作者:码农阿豪@新空间  
在Java编程中,for循环是遍历集合(如List、Set)的常用方式,本文主要介绍了Java在for循环内修改集合的常见陷阱与最佳实践,希望对大家有所帮助

1. 引言

在Java编程中,for循环是遍历集合(如List、Set)的常用方式。然而,许多开发者在循环内部直接对集合进行增删改操作时,往往会遇到ConcurrentModificationException异常。例如:

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
for (Integer num : numbers) {
    if (num % 2 == 0) {
        numbers.remove(num); // 抛出ConcurrentModificationException
    }
}

本文将深入探讨Java集合在循环中修改的问题,分析fail-fast机制,并提供线程安全的修改方案。

2. 问题现象:为什么在for循环中修改集合会出错?

2.1 典型错误示例

(1)增强for循环删除元素

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
    if (s.equals("B")) {
        list.remove(s); // 抛出ConcurrentModificationException
    }
}

异常原因:Java的for-each循环使用Iterator,直接修改集合会导致迭代器状态不一致。

(2)普通for循环删除元素(可能出错)

List<Integer> nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
for (int i = 0; i < nums.size(); i++) {
    if (nums.get(i) % 2 == 0) {
        nums.remove(i); // 可能导致元素跳过
    }
}
// 结果可能是 [1, 3, 4] 而非预期的 [1, 3]

问题:删除元素后列表大小变化,但循环索引继续递增,导致某些元素被跳过。

3. 深入分析:Java集合的fail-fast机制

3.1 什么是fail-fast?

Java的ArrayList、HashSet等非线程安全集合采用fail-fast机制:

当迭代器检测到集合被并发修改(即非通过迭代器自身的方法修改),立即抛出ConcurrentModificationException。

目的是快速失败,避免潜在的数据不一致问题。

3.2 源码分析

以ArrayList为例,其Iterator实现会检查modCount(修改计数器):

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

modCount:集合结构修改次数(如add、remove)。

expectedModCount:迭代器预期的修改次数。

直接调用list.remove()会修改modCount,导致与expectedModCount不一致。

4. 解决方案:安全修改集合的几种方法

4.1 方法1:使用Iterator的remove()方法(推荐)

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
    Integer num = it.next();
    if (num % 2 == 0) {
        it.remove(); // 安全删除
    }
}
System.out.println(numbers); // [1, 3]

优点:

迭代器自身维护modCount,不会触发异常。

适用于单线程环境。

4.2 方法2:使用Java 8+的removeIf()

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
numbers.removeIf(num -> num % 2 == 0);
System.out.println(numbers); // [1, 3]

优点:

代码简洁,内部使用Iterator实现。

性能较好。

4.3 方法3:使用CopyOnWriteArrayList(线程安全)

List<Integer> numbers = new CopyOnWriteArrayList<>(Arrays.asList(1, 2, 3, 4));
for (Integer num : numbers) {
    if (num % 2 == 0) {
        numbers.remove(num); // 安全但性能较低
    }
}
System.out.println(numbers); // [1, 3]

适用场景:

多线程环境。

缺点:每次修改会复制整个数组,性能较差。

4.4 方法4:普通for循环反向遍历

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
for (int i = numbers.size() - 1; i >= 0; i--) {
    if (numbers.get(i) % 2 == 0) {
        numbers.remove(i); // 避免索引错位
    }
}
System.out.println(numbers); // [1, 3]

优点:

无需额外迭代器或副本。

适用于简单删除逻辑。

4.5 方法5:记录待删除元素,最后批量删除

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
List<Integer> toRemove = new ArrayList<>();
for (Integer num : numbers) {
    if (num % 2 == 0) {
        toRemove.add(num);
    }
}
numbers.removeAll(toRemove);
System.out.println(numbers); // [1, 3]

适用场景:

需要复杂条件判断时。

缺点:需要额外空间存储待删除元素。

5. 性能对比:不同方法的效率分析

方法时间复杂度空间复杂度线程安全适用场景
Iterator.remove()O(n)O(1)单线程推荐
removeIf()O(n)O(1)Java 8+简洁写法
CopyOnWriteArrayListO(n²)O(n)多线程环境
反向遍历O(n)O(1)简单删除逻辑
记录后批量删除O(n)O(n)复杂删除条件

结论:

单线程下优先选择Iterator.remove()或removeIf()。

多线程环境使用CopyOnWriteArrayList或加锁。

大数据量避免CopyOnWriteArrayList,选择Iterator或反向遍历。

6. 最佳实践总结

禁止在增强for循环中直接修改集合,改用Iterator.remove()。

Java 8+推荐removeIf(),代码更简洁。

多线程环境使用并发集合(如CopyOnWriteArrayList)或同步块。

大规模数据删除优先选择Iterator或反向遍历。

复杂条件删除可先记录元素,再批量删除。

7. 结论

在Java中,直接于for循环内修改集合会触发ConcurrentModificationException,根源在于fail-fast机制。
安全修改集合的最佳实践包括:

  • 单线程:Iterator.remove()或removeIf()
  • 多线程:CopyOnWriteArrayList或同步控制

掌握这些方法后,可以避免常见陷阱,写出更健壮的Java代码。 

到此这篇关于Java中for循环内修改集合的常见陷阱与最佳实践的文章就介绍到这了,更多相关Java for循环内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 从0开始学习大数据之java spark编程入门与项目实践

    从0开始学习大数据之java spark编程入门与项目实践

    这篇文章主要介绍了从0开始学习大数据之java spark编程入门与项目实践,结合具体入门项目分析了大数据java spark编程项目建立、调试、输出等相关步骤及操作技巧,需要的朋友可以参考下
    2019-11-11
  • Java并发编程中的ReentrantLock类详解

    Java并发编程中的ReentrantLock类详解

    这篇文章主要介绍了Java并发编程中的ReentrantLock类详解,ReentrantLock是juc.locks包中的一个独占式可重入锁,相比synchronized,它可以创建多个条件等待队列,还支持公平/非公平锁、可中断、超时、轮询等特性,需要的朋友可以参考下
    2023-12-12
  • MyBatis常用的jdbcType数据类型

    MyBatis常用的jdbcType数据类型

    这篇文章主要介绍了MyBatis常用的jdbcType数据类型的相关资料,需要的朋友可以参考下
    2016-12-12
  • 浅谈java中的一维数组、二维数组、三维数组、多维数组

    浅谈java中的一维数组、二维数组、三维数组、多维数组

    下面小编就为大家带来一篇浅谈java中的一维数组、二维数组、三维数组、多维数组。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • SpringCloud升级2020.0.x版之OpenFeign简介与使用实现思路

    SpringCloud升级2020.0.x版之OpenFeign简介与使用实现思路

    在微服务系统中,我们经常会进行 RPC 调用。在 Spring Cloud 体系中,RPC 调用一般就是 HTTP 协议的调用。对于每次调用,都要经过一系列详细步骤,接下来通过本文给大家介绍SpringCloud OpenFeign简介与使用,感兴趣的朋友一起看看吧
    2021-10-10
  • Java Swing中的JButton、JComboBox、JList和JColorChooser组件使用案例

    Java Swing中的JButton、JComboBox、JList和JColorChooser组件使用案例

    这篇文章主要介绍了Java Swing中的按钮(JButton)、组合框(JComboBox)、下拉列表(JList)和颜色选择器(JColorChooser)组件使用案例,需要的朋友可以参考下
    2014-10-10
  • Springboot整合SpringSecurity的完整案例详解

    Springboot整合SpringSecurity的完整案例详解

    Spring Security是基于Spring生态圈的,用于提供安全访问控制解决方案的框架,Spring Security登录认证主要涉及两个重要的接口 UserDetailService和UserDetails接口,本文对Springboot整合SpringSecurity过程给大家介绍的非常详细,需要的朋友参考下吧
    2024-01-01
  • 关于post请求内容无法重复获取的解决方法

    关于post请求内容无法重复获取的解决方法

    这篇文章主要介绍了关于post请求内容无法重复获取的解决方法,文中通过代码示例给大家介绍的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-03-03
  • 使用Zookeeper实现分布式锁

    使用Zookeeper实现分布式锁

    这篇文章主要介绍了使用Zookeeper实现分布式锁,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • SpringBoot多文件分布式上传功能实现

    SpringBoot多文件分布式上传功能实现

    本文详细介绍了如何在SpringBoot中实现多文件分布式上传,并用代码给出了相应的实现思路和实现步骤,感兴趣的朋友跟随小编一起看看吧
    2023-06-06

最新评论