如何手写一个迭代器模式?Java 代码怎么实现?
简化版
手写迭代器模式的关键是让集合提供 iterator(),并让迭代器保存当前遍历位置。客户端通过 hasNext() 和 next() 遍历元素,而不是直接访问集合内部数组或节点。
详细版
一个简化实现通常包括:
- 自定义迭代器接口
MyIterator。 - 自定义集合接口
MyIterable。 - 具体集合类保存元素。
- 内部迭代器类维护游标。
Java 中也可以直接实现标准的 Iterable<T> 和 Iterator<T>,这样对象就能被增强 for 循环遍历。
完整版教学
一、先定义自定义接口
为了理解模式,可以先写最小版本:
interface MyIterator<E> {
boolean hasNext();
E next();
}
interface MyIterable<E> {
MyIterator<E> iterator();
}
MyIterable 表示“可被遍历”,MyIterator 表示“具体怎么遍历”。
二、实现一个简单集合
class NameList implements MyIterable<String> {
private final String[] names;
public NameList(String[] names) {
this.names = names;
}
@Override
public MyIterator<String> iterator() {
return new NameIterator();
}
private class NameIterator implements MyIterator<String> {
private int cursor = 0;
@Override
public boolean hasNext() {
return cursor < names.length;
}
@Override
public String next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return names[cursor++];
}
}
}
这里 NameIterator 作为内部类,可以访问外部集合的 names,同时自己维护 cursor。
三、客户端使用
NameList list = new NameList(new String[]{"Tom", "Jerry", "Alice"});
MyIterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
客户端不知道 NameList 内部是数组,也不需要知道下标如何变化。
四、实现 Java 标准接口
实际 Java 项目里,更常见的是实现标准接口:
class NameList implements Iterable<String> {
private final String[] names;
public NameList(String[] names) {
this.names = names;
}
@Override
public Iterator<String> iterator() {
return new Iterator<>() {
private int cursor = 0;
@Override
public boolean hasNext() {
return cursor < names.length;
}
@Override
public String next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return names[cursor++];
}
};
}
}
实现 Iterable 后,就可以使用增强 for:
for (String name : new NameList(new String[]{"Tom", "Jerry"})) {
System.out.println(name);
}
增强 for 底层就是调用 iterator()。
五、为什么 next 要抛异常
如果没有下一个元素还调用 next(),Java 标准 Iterator 约定应抛出 NoSuchElementException。
这比返回 null 更明确,因为集合中本身可能允许 null 元素。返回 null 会让调用方无法区分“没有元素”和“元素就是 null”。
六、remove 方法要谨慎实现
Java 的 Iterator 还有默认 remove() 方法。它用于删除最近一次 next() 返回的元素。
如果不支持删除,可以抛出 UnsupportedOperationException。不要写一个行为不清楚的空实现,否则调用方以为删除成功,实际数据没有变化。
七、常见误区与追问
实现迭代器要把游标含义、边界和修改策略写清楚。长度为3的数组可令 cursor 表示“下一个返回下标”,初始0,每次 next 后递增,到3时 hasNext 为 false。next() 仍必须自行检查边界,不能假设调用者一定先调用 hasNext。
| 检查维度 | 判定依据 |
|---|---|
| cursor 初值 | 指向第一个待返回元素 |
| next 后 | 返回当前元素并推进游标 |
next(): check cursor<3 -> data[cursor++]
易错点:hasNext 是查询,不应偷偷推进游标;推进只能发生在 next。
- 误区:调用 hasNext 会移动到下一个元素。 标准语义只判断是否存在下一项,可反复调用且结果稳定。
- 追问:next 为什么要重复边界检查? 调用者可以直接调用 next,迭代器必须自己维护契约。
- 误区:所有迭代器都必须支持 remove。 可抛
UnsupportedOperationException,但应在 API 中明确。 - 追问:链表迭代器如何做到 O(1) next? 保存当前节点引用,而不是每次按下标从头查找。
- 追问:怎样支持 fail-fast? 创建时记录结构版本,每次关键操作比较集合当前版本。
八、加强记忆
手写迭代器模式抓住三件事:集合提供 iterator(),迭代器保存游标,客户端只通过 hasNext() 和 next() 访问元素。能实现 Iterable 时,增强 for 就能自然接入。