迭代器与遍历机制
遍历是集合最常见的操作之一,但它背后包含 Iterable、Iterator、增强 for、fail-fast、结构性修改、删除元素等机制。很多 ConcurrentModificationException 都来自对遍历机制理解不清。
# 1. Iterable
Iterable 表示一个对象可以被迭代。
public interface Iterable<T> {
Iterator<T> iterator();
}
只要实现了 Iterable,就可以使用增强 for:
for (String name : names) {
System.out.println(name);
}
集合遍历模型:
Collection 实现 Iterable
│
▼
iterator()
│
▼
Iterator
│
├─ hasNext()
├─ next()
└─ remove()
# 2. Iterator
Iterator 用于逐个访问集合元素。
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
核心方法:
| 方法 | 作用 |
|---|---|
hasNext() | 是否还有下一个元素 |
next() | 返回下一个元素 |
remove() | 删除上一次 next() 返回的元素 |
remove() 只能在 next() 之后调用,否则会抛 IllegalStateException。
# 3. 增强 for 的本质
增强 for:
for (String name : names) {
System.out.println(name);
}
大致等价于:
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
因此增强 for 遍历集合时,本质仍然使用迭代器。
数组的增强 for 不走 Iterator,而是按下标遍历。
# 4. 结构性修改
结构性修改指会改变集合大小或内部结构的操作。
常见结构性修改:
addremoveclearputresize
非结构性修改:
ArrayList.set(index, value)Map.Entry.setValue
结构性修改会影响迭代器的预期状态。
# 5. fail-fast 机制
很多集合维护一个修改次数 modCount。迭代器创建时记录 expectedModCount。
集合 modCount = 3
│ 创建迭代器
▼
迭代器 expectedModCount = 3
遍历中集合 add/remove
│
▼
集合 modCount = 4
迭代器 next 时检查:
modCount != expectedModCount
│
▼
ConcurrentModificationException
示例:
for (String name : names) {
if (name.startsWith("A")) {
names.remove(name);
}
}
这可能抛出 ConcurrentModificationException。
注意:fail-fast 是尽力而为的错误检测,不是并发安全保证。
# 6. 遍历时删除元素
正确方式一:使用迭代器删除。
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
if (name.startsWith("A")) {
iterator.remove();
}
}
正确方式二:使用 removeIf。
names.removeIf(name -> name.startsWith("A"));
正确方式三:创建新集合。
List<String> result = names.stream()
.filter(name -> !name.startsWith("A"))
.toList();
如果原集合需要保留,创建新集合更安全。
# 7. ListIterator
ListIterator 是 List 专用迭代器,支持双向遍历和修改。
ListIterator<String> iterator = names.listIterator();
while (iterator.hasNext()) {
String name = iterator.next();
if ("A".equals(name)) {
iterator.set("AA");
}
}
方法:
| 方法 | 作用 |
|---|---|
hasPrevious() | 是否有前一个元素 |
previous() | 返回前一个元素 |
nextIndex() | 下一个元素索引 |
previousIndex() | 前一个元素索引 |
set(e) | 替换上一次访问的元素 |
add(e) | 在当前位置添加元素 |
ListIterator 适合需要边遍历边局部修改 List 的场景。
# 8. Map 的遍历
推荐遍历 entrySet:
for (Map.Entry<Long, User> entry : userMap.entrySet()) {
Long id = entry.getKey();
User user = entry.getValue();
}
只遍历 key:
for (Long id : userMap.keySet()) {
}
只遍历 value:
for (User user : userMap.values()) {
}
避免:
for (Long id : userMap.keySet()) {
User user = userMap.get(id);
}
这会多一次 hash 查找。
# 9. Stream 遍历
Stream 更适合表达数据处理流水线:
List<String> activeNames = users.stream()
.filter(User::isActive)
.map(User::getName)
.toList();
处理流程:
source
│
▼
filter
│
▼
map
│
▼
collect / toList
Stream 不应被滥用在有大量副作用的逻辑里:
users.stream().forEach(user -> externalService.call(user));
如果主要是执行动作而不是转换数据,普通循环更清晰。
# 10. 并行遍历
parallelStream() 会把任务提交到公共 ForkJoinPool。
users.parallelStream()
.map(this::calculate)
.toList();
适合:
- CPU 密集型。
- 数据量足够大。
- 单个元素处理相对独立。
- 没有共享可变状态。
不适合:
- IO 密集且阻塞不可控。
- 数据量很小。
- 需要保持严格顺序。
- 依赖 ThreadLocal 上下文。
- 会修改共享集合。
并行流不是“加速开关”,使用前要压测。
# 11. fail-safe 迭代
某些并发集合使用弱一致性迭代器,不会在并发修改时抛 fail-fast。
例如:
ConcurrentHashMapCopyOnWriteArrayList
CopyOnWriteArrayList 的迭代器基于快照:
创建迭代器
│
▼
持有当前数组快照
│
▼
后续写操作复制新数组
│
▼
迭代器仍遍历旧快照
这适合读多写少场景,但写入成本高。
# 12. 遍历性能建议
| 场景 | 建议 |
|---|---|
ArrayList 普通遍历 | 增强 for 或普通 for 都可以 |
LinkedList 遍历 | 使用增强 for 或 iterator,避免循环 get(i) |
Map 同时要 key/value | 遍历 entrySet |
| 遍历中删除 | 使用 iterator.remove 或 removeIf |
| 数据转换 | 使用 Stream |
| 高性能热点路径 | 写清晰循环并压测 |
# Tips 快问快答
Q:增强 for 的底层是什么?
A:遍历集合时底层是 Iterator,遍历数组时是下标循环。
Q:为什么遍历时直接 remove 会报错?
A:集合的 modCount 和迭代器的 expectedModCount 不一致,触发 fail-fast。
Q:遍历时删除元素的推荐方式是什么?
A:使用 Iterator.remove() 或 removeIf()。
Q:fail-fast 是线程安全机制吗? A:不是,它只是尽力发现错误修改。
Q:ListIterator 比 Iterator 多什么?
A:支持双向遍历、设置元素和在当前位置添加元素。
Q:Map 遍历为什么推荐 entrySet?
A:同时拿 key 和 value,避免二次查找。
Q:parallelStream 一定更快吗?
A:不一定。它有调度开销,还可能受共享状态和阻塞影响。
Q:CopyOnWriteArrayList 遍历为什么不会报并发修改? A:迭代器遍历的是创建时的数组快照。
Q:Stream 适合替代所有循环吗? A:不适合。复杂副作用逻辑用普通循环更清楚。
Q:LinkedList 为什么不要用 for+i+get 遍历?
A:每次 get(i) 都要遍历链表,整体可能退化为 O(n²)。