java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java遍历集合报错ConcurrentModificationException

Java遍历集合时删除元素抛出异常ConcurrentModificationException的排查与解决

作者:tianyu234

Java遍历集合时删除元素,你是否遇到过 ConcurrentModificationException异常,本文从Iterator接口设计、fail-fast底层原理到ListIterator双向遍历,再到自定义迭代器实战,帮你彻底搞懂Java迭代器机制,掌握安全删除元素、避免并发修改异常的实用技巧

从 ConcurrentModificationException 到 fail-safe,从 ListIterator 到自定义迭代器,一篇带你真正掌握迭代器

我们在日常开发中,几乎天天都在遍历集合。但你有没有想过:

这些问题的答案,都藏在 迭代器(Iterator) 的设计哲学里。今天这篇文章,我们从接口设计、快速失败(fail-fast)机制、双向遍历(ListIterator)、自定义实现到生产级避坑,把 Iterator 彻底讲透。

一、先上结论(一张表看懂核心角色)

角色接口/类核心职责关键方法特点
可迭代对象Iterable<T>返回迭代器(被遍历的对象)iterator()增强 for 循环的“入场券”
迭代器(单向)Iterator<E>单向遍历并安全删除hasNext()、next()、remove()最基础、最通用
迭代器(双向)ListIterator<E>双向遍历,支持增/改hasPrevious()、previous()、add()、set()仅限 List 及其子类
拆分迭代器Spliterator<E>并行遍历(Java 8+)trySplit()、forEachRemaining()Stream 并行流的底层

一句话记忆口诀:

二、Iterator基础:为什么不用普通for循环?

1. 三种遍历方式对比

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

// 方式1:普通 for 循环(依赖下标)
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

// 方式2:增强 for 循环(语法糖,底层也是迭代器)
for (String s : list) {
    System.out.println(s);
}

// 方式3:显式迭代器(最灵活)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

2. 为什么需要迭代器?(解决了什么问题)

  1. 统一遍历接口:无论 List、Set、Queue,遍历方式完全一致(解耦数据结构)。
  2. 安全的删除操作:普通 for 循环删除元素会导致索引错乱;增强 for 循环直接删除会抛异常;只有 Iterator.remove() 才是安全的。
  3. 隐藏底层实现:你不需要关心是数组还是链表,只管 hasNext() + next()。

三、核心方法详解(hasNext、next、remove)

1.hasNext()和next()(遍历的基本动作)

Iterator<String> it = list.iterator();
while (it.hasNext()) {   // 1. 问:还有吗?
    String s = it.next(); // 2. 拿:拿到下一个,并且游标后移
    System.out.println(s);
}

注意:如果没有调用 hasNext() 直接调用 next(),当没有元素时会抛出 NoSuchElementException。

2.remove()(唯一安全的删除方式)

这是迭代器最核心的实用价值。

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

// ✅ 正确:使用迭代器删除偶数
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
    Integer num = it.next();
    if (num % 2 == 0) {
        it.remove();  // 安全删除当前元素
    }
}
System.out.println(numbers);  // [1, 3, 5]

3. remove()的两条铁律(面试常问)

  1. 每次 next() 后只能调用一次 remove()。
  2. 必须先调用 next(),再调用 remove()(不能连续删除,也不能在 next() 之前删除)。
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    // it.remove();  // ❌ 报错!IllegalStateException(没有先 next)
    Integer num = it.next();
    it.remove();
    // it.remove();  // ❌ 报错!IllegalStateException(连续删除两次)
}

四、增强for循环的“甜蜜陷阱”(重点)

语法糖:for (String s : list) 编译后就是 while (it.hasNext()) { String s = it.next(); ... }。

致命错误:在增强 for 循环中直接调用 list.remove()。

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

// ❌ 错误!抛出 ConcurrentModificationException
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s);  // 直接操作集合,导致迭代器检测到 modCount 变化
    }
}

为什么报错?

因为增强 for 底层隐式创建了迭代器。集合内部有一个 modCount 计数器(记录修改次数)。list.remove() 会修改 modCount,但迭代器内部的 expectedModCount 没有同步更新,导致两者不相等,迭代器检测到后立即抛出 ConcurrentModificationException(快速失败机制,见下一节)。

正确做法:使用显式 Iterator 调用 it.remove()。

五、ListIterator:双向迭代器(支持增、改)

ListIterator 是 Iterator 的子接口,专门为 List 提供,支持双向遍历和元素修改/添加。

1.ListIteratorvsIterator对比

功能IteratorListIterator
方向只能从前到后双向(前/后)
删除✅ remove()✅ remove()
添加❌ 不支持✅ add(E e)
修改❌ 不支持✅ set(E e)
获取索引❌ 不支持✅ nextIndex() / previousIndex()
适用范围Collection 所有子类仅限 List(含 ArrayList、LinkedList)

2. 使用示例(双向遍历 + 添加)

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

ListIterator<String> lit = list.listIterator();
// 1. 从前往后遍历
while (lit.hasNext()) {
    String s = lit.next();
    if ("B".equals(s)) {
        lit.add("C");   // 在 B 后面插入 C(游标自动跳过新元素)
    }
}
System.out.println(list);  // [A, B, C, D]

// 2. 从后往前遍历
while (lit.hasPrevious()) {
    String s = lit.previous();
    if ("C".equals(s)) {
        lit.set("X");   // 修改当前元素为 X
    }
}
System.out.println(list);  // [A, B, X, D]

核心机制:add() 插入的元素位于当前游标之前(即 next() 返回的元素之前)。set() 更新的是上一次 next() 或 previous() 返回的元素。

六、ConcurrentModificationException:快速失败(fail-fast)机制

1. 什么是 fail-fast?

Java 集合(ArrayList、HashMap 等)的迭代器在设计时采用了快速失败机制:当迭代器被创建后,如果集合内容被非迭代器方式(如直接 list.remove())修改,迭代器会立即抛出 ConcurrentModificationException,而不是冒着风险继续执行。

2. 底层原理(源码视角)

// AbstractList 源码片段
public abstract class AbstractList<E> {
    protected transient int modCount = 0;  // 集合结构修改次数
    
    // 迭代器内部
    private class Itr implements Iterator<E> {
        int expectedModCount = modCount;  // 创建迭代器时保存的快照
        
        public E next() {
            checkForComodification();  // 每次 next 都检查
            // ...
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
}

当 list.remove() 被调用时,modCount++;但迭代器的 expectedModCount 没变。下次调用 it.next() 时发现两者不等,直接抛异常。

3. 如何正确避免?

七、安全失败(fail-safe)机制

1. 什么是 fail-safe?

与 fail-fast 对应,安全失败迭代器在遍历时,不会抛出异常。因为它们操作的是原集合的克隆/快照,修改原集合不会影响遍历中的快照。

2. 典型代表

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");

for (String s : list) {  // 底层是 CopyOnWrite 迭代器(安全失败)
    if ("A".equals(s)) {
        list.add("C");   // ✅ 不会抛异常!因为遍历的是快照
    }
}
System.out.println(list);  // [A, B, C](新增元素不影响正在进行的遍历)

八、自定义迭代器实现(面试实战)

如果让你设计一个支持迭代的自定义集合,必须实现 Iterable 接口并重写 iterator() 方法。

public class MyCollection<T> implements Iterable<T> {
    private Object[] elements = new Object[10];
    private int size = 0;

    public void add(T item) {
        elements[size++] = item;
    }

    // 1. 实现 Iterable 接口,返回迭代器
    @Override
    public Iterator<T> iterator() {
        return new MyIterator();
    }

    // 2. 自定义迭代器内部类
    private class MyIterator implements Iterator<T> {
        private int cursor = 0;

        @Override
        public boolean hasNext() {
            return cursor < size;
        }

        @SuppressWarnings("unchecked")
        @Override
        public T next() {
            if (!hasNext()) throw new NoSuchElementException();
            return (T) elements[cursor++];
        }

        // 简化版本,不支持 remove
    }

    public static void main(String[] args) {
        MyCollection<String> mc = new MyCollection<>();
        mc.add("Java");
        mc.add("Iterator");
        for (String s : mc) {  // 增强 for 循环生效!
            System.out.println(s);
        }
    }
}

九、Java 8 新特性:forEachRemaining()

Iterator 接口在 Java 8 中添加了一个默认方法 forEachRemaining(Consumer<? super E> action),可以一次性消费剩余所有元素。

List<String> list = Arrays.asList("A", "B", "C", "D");
Iterator<String> it = list.iterator();

it.next();  // 跳过 A
it.forEachRemaining(System.out::println);  // 输出 B C D(从当前位置到末尾)

十、思考题(检验是否真的懂了)

// 问题 1:下面代码会抛出什么异常?为什么?
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s);
    }
}
// 问题 2:下面代码中,ArrayList 和 CopyOnWriteArrayList 的表现有什么不同?
// 场景 A:ArrayList
List<Integer> list1 = new ArrayList<>(Arrays.asList(1,2,3));
for(Integer i : list1) { if(i==2) list1.remove(i); }

// 场景 B:CopyOnWriteArrayList
List<Integer> list2 = new CopyOnWriteArrayList<>(Arrays.asList(1,2,3));
for(Integer i : list2) { if(i==2) list2.remove(i); }
// 问题 3:ListIterator 和 Iterator 都能删除元素,为什么 ListIterator 还能添加元素?

答案(选中下方空白区域查看):

  1. 抛出 ConcurrentModificationException。增强 for 底层是迭代器,list.remove() 修改了 modCount,但迭代器内部的 expectedModCount 没变,导致冲突。
  2. 场景 A 抛出 ConcurrentModificationException(fail-fast);场景 B 正常运行,因为 CopyOnWriteArrayList 是 fail-safe 机制,遍历的是快照,修改原集合不影响遍历。
  3. ListIterator 是专门为 List 设计的,因为 List 有索引结构,add() 方法可以精确控制插入位置(通过游标)。而 Set 是无序的,不支持 add() 到指定位置,所以普通的 Iterator 不提供 add(),以保证所有集合的一致性和安全性。

总结(终极速查表)

知识点一句话记忆
Iterator 核心方法hasNext() 问路,next() 走路,remove() 回头清除
Iterable实现这个接口,才能用增强 for 循环
增强 for底层是迭代器,直接 list.remove() 会翻车
ListIterator双向走,还能增删改,但只能用在 List 上
fail-fast(快速失败)ArrayList、HashMap 默认,检测到并发修改立即抛异常
fail-safe(安全失败)CopyOnWriteArrayList、ConcurrentHashMap,遍历快照,安全但不保证实时一致性
forEachRemainingJava 8 提供,一次性消费剩余所有元素

以上就是Java遍历集合时删除元素抛出异常ConcurrentModificationException的排查与解决的详细内容,更多关于Java遍历集合报错ConcurrentModificationException的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文