☰
【Java 集合】Iterator 迭代器详解:next、remove 与 fail-fast 机制一次讲透
2026/11/2 1:45:57 网站建设 项目流程

一、前言

只要用过 Java 集合,就一定见过Iterator。但很多人对它只有模糊的印象:

  • 为什么遍历时不能直接list.add()/list.remove()?
  • it.remove()为什么就安全?
  • 删除元素后,游标到底在哪?后面的元素会不会被漏掉?

本质原因:Iterator是一个"隐式游标"模型,游标永远夹在两个元素之间,并且内部有modCount校验机制在兜底。

这篇文章用 3 个递进的例子,把Iterator的游标行为一次性讲透。


二、Iterator 核心方法速查

方法作用对 cursor 的影响
hasNext()判断游标后面是否还有元素不变,不检查 modCount
next()返回游标后面的元素,游标后移cursor = i + 1,lastRet = i
remove()删除上一次 next 返回的那个元素游标回退到lastRet
记住一句话:remove()删的是"刚读过的那个",然后把游标拉回来,所以后面的元素不会被跳过。

三、例 1:Iterator 的 next / remove

3.1 代码

import java.util.ArrayList;
import java.util.Iterator;

public class TestIteratorRemove {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
System.out.println(item);
if ("B".equals(item)) {
it.remove(); // 删除刚刚 next 取出的 B
}
}
System.out.println("最终集合:" + list);
}
}

3.2 一步一步模拟执行

初始状态:list = [A, B, C],cursor = 0,lastRet = -1

初始: [A, B, C] cursor=0

next() → A cursor=1 lastRet=0 打印 A
next() → B cursor=2 lastRet=1 打印 B → it.remove()
└─ ArrayList.this.remove(1) → [A, C]
cursor = lastRet = 1 ← 游标回退
lastRet = -1
hasNext() cursor(1) != size(2) → true
next() → index 1 = C cursor=2 打印 C
hasNext() cursor(2) == size(2) → false → 退出

关键点:it.remove()会把cursor回退到lastRet,所以后面的 C 不会被漏掉。

3.3 真实输出

A
B
C
最终集合:[A, C]

3.4 源码印证

// ArrayList.Itr#remove
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet); // 删掉刚读过的元素
cursor = lastRet; // 游标回退
lastRet = -1; // 重置
expectedModCount = modCount; // 同步 modCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

3.5 附加坑点:连续两次 remove 会抛异常

因为remove()之后lastRet = -1,此时再调一次会抛:

java.lang.IllegalStateException
it.next(); // 返回 B
it.remove(); // ✅ 删除 B,lastRet = -1
it.remove(); // ❌ IllegalStateException

四、例 2:遍历中直接操作集合 → ConcurrentModificationException

4.1 代码

import java.util.ArrayList;
import java.util.Iterator;

public class TestError {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
System.out.println(item);
if ("B".equals(item)) {
list.add("X"); // ❌ 直接调用集合的 add,而不是迭代器的 add
}
}
}
}

4.2 输出

A
B
Exception in thread "main" java.util.ConcurrentModificationException

4.3 为什么会抛异常?

ArrayList的迭代器是fail-fast(快速失败)的:

// ArrayList.Itr
int expectedModCount = modCount; // 创建迭代器时记录

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
  • list.add()/list.remove()只会修改modCount
  • 不会同步更新迭代器内部的expectedModCount
  • 下一次next()/remove()调用checkForComodification()时直接抛异常

注意:hasNext()不检查modCount,所以异常是在下一次next()时抛出的,不是add的瞬间。

4.4 如果不考虑 CME,会发生什么?

这才是"真正危险"的地方——它不一定报错,而是给你一个错误的结果。

场景后果
list.add("X")加到末尾size不断增加,cursor也随next()增加,hasNext()可能永远为true→无限循环
list.add(0, "X")插到前面后续元素整体后移,迭代器按索引继续走 →重复遍历已读元素 / 跳过未读元素 / 遍历到新加元素
LinkedList节点指针和size不同步 → 可能漏元素、重复元素,甚至NullPointerException
CopyOnWriteArrayList迭代器是快照,不会抛 CME,但看不到新增元素

五、例 3:删除场景 —— 比 add 更容易暴露 bug

先看代码(这是错误示范):

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");

Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
if ("B".equals(s)) {
list.remove(1); // ❌ 直接删集合,删除 B
}
}

5.1 实际运行结果

A
B
Exception in thread "main" java.util.ConcurrentModificationException

因为list.remove(1)修改了modCount,下一次it.next()立刻 fail-fast。

5.2 假如没有 fail-fast 检查,会漏元素

如果忽略modCount检查(比如自己写一个不带校验的迭代器),问题就暴露了:

初始: [A, B, C, D] cursor=0

next() → A cursor=1 打印 A
next() → B cursor=2 打印 B → list.remove(1)
└─ [A, C, D] size=3
C 挪到 index=1,D 挪到 index=2
cursor 仍然是 2(不变!)

hasNext() cursor(2) != size(3) → true
next() → index 2 = D 打印 D ❌ C 被跳过了!
hasNext() cursor(3) == size(3) → false

结果:打印A B D,C 被静默漏掉。这种 bug 比直接抛异常可怕得多——它不会报错,只会悄悄丢数据。

5.3 正确写法

// 方案一:用 Iterator 自己的 remove
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("B".equals(s)) {
it.remove(); // ✅ 会同步 modCount,并回退 cursor
}
}

// 方案二:用 removeIf(JDK 8+,最简洁)
list.removeIf(s -> "B".equals(s));

// 方案三:CopyOnWriteArrayList(并发场景)

六、Iterator 与 ListIterator 的关系

很多人会把这两个混为一谈。准确说:

ListIterator是Iterator的子接口,但Iterator不一定是ListIterator。

类比:苹果是水果,但水果不一定是苹果。

维度IteratorListIterator
继承关系顶层接口extends Iterator
适用集合所有Collection只能用于List
遍历方向单向next()双向next()/previous()
修改方法只有remove()remove()、set()、add()
获取方式collection.iterator()list.listIterator()

源码里也是两个不同的实现:

private class Itr implements Iterator<E> {
// 只有 hasNext、next、remove
}

private class ListItr extends Itr implements ListIterator<E> {
// 多了 hasPrevious、previous、set、add、nextIndex、previousIndex
}

所以下面这样写会编译报错:

Iterator<String> it = list.iterator();
it.next();
it.set("X"); // ❌ 编译错误:Iterator 没有 set()
it.add("Y"); // ❌ 编译错误:Iterator 没有 add()

想用add()、set()、previous(),必须写:

ListIterator<String> lit = list.listIterator();

(一)两边完全相同的规则

  1. 迭代器新建:lastRet = -1,代表没有成功读取元素
  2. next():更新lastRet为本次取出元素下标 ✅ 两者一致
  3. remove():
    • 删除lastRet记录的元素
    • 执行完 remove 后,强制lastRet = -1✅ 两者一致
  4. hasNext():不会修改 lastRet✅ 两者一致
  5. 如果lastRet == -1,调用remove()→ 抛IllegalStateException✅ 两者一致

(二)仅 ListIterator 才有(Iterator 没有这些方法)

  • previous():更新lastRet = cursor - 1(本次取出元素下标)
  • add():执行完强制lastRet = -1
  • set():不修改 lastRet,依赖已有 lastRet;lastRet=-1 调用 set 抛异常
  • nextIndex()/previousIndex():只读,不修改lastRet
👉 普通Iterator根本不存在previous()、add()、set()、nextIndex、previousIndex,自然谈不上对 lastRet 的影响。

七、总结

7.1 一句话记住每个方法的游标行为

操作cursor 变化下一个next()读到
next()+1下一个元素
remove()回退到lastRet原来游标后面的那个元素

7.2 三条铁律

  1. 遍历期间只用迭代器的方法(it.remove()),绝不直接调list.add()/list.remove()。
  2. remove()之前必须先next(),连续两次remove()会抛IllegalStateException。
  3. Iterator只能单向遍历、只能删除,想双向遍历、想插入 / 修改,请用ListIterator。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询