使用迭代器模式有哪些常见误区?
简化版
迭代器模式常见误区包括:把迭代器和集合混成一个对象、遍历时直接修改集合、以为 fail-fast 等于线程安全、忘记 next() 越界要抛异常、把所有简单循环都强行抽成迭代器。
详细版
迭代器模式的关键是“遍历状态独立于集合”。如果集合自己保存全局游标,多个遍历过程会互相干扰。
常见错误有:
- 集合自己实现迭代器并保存唯一游标。
- 增强 for 中调用集合
remove()。 - 认为
ConcurrentModificationException能保证并发安全。 next()没有元素时返回null。- 对简单固定遍历过度设计。
- 迭代器暴露过多集合内部结构。
这些问题本质上都破坏了迭代器的职责边界或使用约定。
完整版教学
一、误区一:集合自己保存遍历游标
如果集合内部只有一个游标,那么同一个集合同时遍历两次就会出问题。
例如:
Iterator<String> it1 = list.iterator();
Iterator<String> it2 = list.iterator();
如果 it1 和 it2 实际共享同一个游标,两个遍历过程就会互相影响。
正确做法是每次调用 iterator() 返回新的迭代器对象,每个迭代器保存自己的状态。
二、误区二:遍历时直接修改集合
增强 for 背后使用迭代器。如果循环中直接调用集合的 remove(),容易触发 fail-fast。
不推荐:
for (String s : list) {
if (s.startsWith("A")) {
list.remove(s);
}
}
推荐:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.startsWith("A")) {
it.remove();
}
}
或者使用 removeIf(),让集合自己处理遍历删除。
三、误区三:认为 fail-fast 保证线程安全
fail-fast 只是检测到结构性修改时尽快抛异常,不是同步机制。
它不保证:
- 一定能检测到所有并发修改。
- 数据在并发环境中一定一致。
- 多线程访问一定安全。
需要线程安全时,应使用并发集合、同步包装或显式锁。
四、误区四:next 越界返回 null
Iterator.next() 在没有元素时应该抛出 NoSuchElementException。
如果返回 null,调用方无法判断是集合中真的有 null,还是已经遍历结束。
这会让错误被隐藏,后续更难排查。
五、误区五:迭代器泄露内部结构
迭代器应该提供遍历能力,而不是把底层数组、链表节点直接暴露出去。
如果迭代器返回内部节点对象,客户端仍然可能依赖集合实现细节。这样就没有达到封装目的。
六、误区六:所有循环都抽成迭代器
如果只是一个局部数组循环,没有复用需求,也不涉及封装内部结构,普通 for 循环就很好。
迭代器模式适合稳定抽象和复用遍历逻辑,不适合为了模式而制造额外层次。
七、常见误区与追问
迭代器错误通常集中在游标契约与并发修改。含3个元素的集合,合法调用序列应是 hasNext -> next 重复3次,第4次 next 必须抛 NoSuchElementException,不能返回 null 混淆真实空元素。删除操作也必须定义是否只能在成功 next 后调用一次。
| 检查维度 | 判定依据 |
|---|---|
| 遍历结束 | hasNext=false,next 抛异常 |
| 删除契约 | 只删除最近一次返回的元素 |
cursor: 0 -> 1 -> 2 -> 3(end)
易错点:Iterator 是有状态游标,同一个实例不能当作可重复遍历的集合。
- 误区:next 在末尾返回 null 更友好。 集合可能合法包含 null,标准契约用异常区分越界。
- 追问:为什么每次 foreach 都能重新遍历? Iterable 每次调用 iterator() 应创建新的游标实例。
- 误区:遍历时可直接调用集合 remove。 这会绕过迭代器状态并可能触发并发修改检测。
- 追问:remove 连续调用两次会怎样? 标准 Iterator 契约通常抛
IllegalStateException。 - 追问:空集合怎么测试? 首次 hasNext 为 false,首次 next 就抛
NoSuchElementException。
八、加强记忆
迭代器模式最容易错在“边遍历边乱改”和“游标放错地方”。记住:游标属于迭代器,结构属于集合;修改集合要遵守迭代器约定,fail-fast 只是提醒,不是并发安全护身符。