Java遍历集合时删除元素抛出异常ConcurrentModificationException的排查与解决
作者:tianyu234
从 ConcurrentModificationException 到 fail-safe,从 ListIterator 到自定义迭代器,一篇带你真正掌握迭代器
我们在日常开发中,几乎天天都在遍历集合。但你有没有想过:
- 为什么在遍历
ArrayList时,用list.remove()会抛异常,但用iterator.remove()却没事? - 为什么
ConcurrentHashMap的迭代器不会抛异常? Iterator和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 并行流的底层 |
一句话记忆口诀:
- Iterable → 容器本身(能产生迭代器)。
- Iterator → 单向箭头(只能往前走,能删)。
- ListIterator → 双向箭头(能前能后,能增能改)。
- Spliterator → 多箭头(分块并行)。
二、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. 为什么需要迭代器?(解决了什么问题)
- 统一遍历接口:无论
List、Set、Queue,遍历方式完全一致(解耦数据结构)。 - 安全的删除操作:普通
for循环删除元素会导致索引错乱;增强for循环直接删除会抛异常;只有Iterator.remove()才是安全的。 - 隐藏底层实现:你不需要关心是数组还是链表,只管
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()的两条铁律(面试常问)
- 每次 next() 后只能调用一次 remove()。
- 必须先调用 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对比
| 功能 | Iterator | ListIterator |
|---|---|---|
| 方向 | 只能从前到后 | 双向(前/后) |
| 删除 | ✅ 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. 如何正确避免?
- 单线程:使用
Iterator.remove()(它会同步更新expectedModCount)。 - 多线程:使用
ConcurrentHashMap、CopyOnWriteArrayList等安全失败(fail-safe) 集合,或者加锁(synchronized)保护迭代代码块。
七、安全失败(fail-safe)机制
1. 什么是 fail-safe?
与 fail-fast 对应,安全失败迭代器在遍历时,不会抛出异常。因为它们操作的是原集合的克隆/快照,修改原集合不会影响遍历中的快照。
2. 典型代表
ConcurrentHashMap的迭代器(弱一致性)CopyOnWriteArrayList的迭代器(基于快照)
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 还能添加元素?
答案(选中下方空白区域查看):
- 抛出
ConcurrentModificationException。增强for底层是迭代器,list.remove()修改了modCount,但迭代器内部的expectedModCount没变,导致冲突。 - 场景 A 抛出
ConcurrentModificationException(fail-fast);场景 B 正常运行,因为CopyOnWriteArrayList是 fail-safe 机制,遍历的是快照,修改原集合不影响遍历。 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,遍历快照,安全但不保证实时一致性 |
| forEachRemaining | Java 8 提供,一次性消费剩余所有元素 |
以上就是Java遍历集合时删除元素抛出异常ConcurrentModificationException的排查与解决的详细内容,更多关于Java遍历集合报错ConcurrentModificationException的资料请关注脚本之家其它相关文章!
