一、前言
只要用过 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.IllegalStateExceptionit.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.ConcurrentModificationException4.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。
类比:苹果是水果,但水果不一定是苹果。
| 维度 | Iterator | ListIterator |
|---|---|---|
| 继承关系 | 顶层接口 | 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();(一)两边完全相同的规则
- 迭代器新建:
lastRet = -1,代表没有成功读取元素 next():更新lastRet为本次取出元素下标 ✅ 两者一致remove():- 删除
lastRet记录的元素 - 执行完 remove 后,强制
lastRet = -1✅ 两者一致
- 删除
hasNext():不会修改 lastRet✅ 两者一致- 如果
lastRet == -1,调用remove()→ 抛IllegalStateException✅ 两者一致
(二)仅 ListIterator 才有(Iterator 没有这些方法)
previous():更新lastRet = cursor - 1(本次取出元素下标)add():执行完强制lastRet = -1set():不修改 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 三条铁律
- 遍历期间只用迭代器的方法(
it.remove()),绝不直接调list.add()/list.remove()。 remove()之前必须先next(),连续两次remove()会抛IllegalStateException。Iterator只能单向遍历、只能删除,想双向遍历、想插入 / 修改,请用ListIterator。