18|迭代器:把遍历状态从集合中取出来
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.iterator。
系列导航:Java 25 设计模式学习指南
阅读列表需要顺序展示文章。若集合自身保存一个 currentIndex,那么两个调用方同时遍历同一个列表就会争抢游标;若直接把内部 ArrayList 暴露出去,调用者又可以绕过集合的编辑规则。
迭代器让每一次遍历拥有自己的位置,并通过统一协议逐项访问。本例还选择快照语义:开始遍历之后新增的标题,不进入这次遍历。
从问题提炼设计意图
迭代器在不暴露聚合对象内部表示的前提下顺序访问其元素,把遍历过程中的游标状态封装为独立对象。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
ReadingList |
聚合对象,实现 Iterable |
SnapshotIterator |
具体迭代器,保存快照与独立游标 |
Iterator<String> |
遍历协议 |
Demo |
使用增强 for,不感知内部列表结构 |
classDiagram
ReadingList ..> SnapshotIterator : creates
SnapshotIterator ..|> Iterator
ReadingList ..|> Iterable
Demo ..> ReadingList : iterates
代码思路:变化应该落在哪个对象上
ReadingList.add 管理可变数据;iterator 每次执行 List.copyOf,得到本次遍历冻结的内容,并创建新的 SnapshotIterator。集合状态与遍历状态分开,所以两个 iterator 不会共享 index。
hasNext 只检查位置,不推进游标;next 先验证是否还有元素,再取值并递增。耗尽时抛 NoSuchElementException,不能返回 null,因为 null 本身可能是其他集合允许存放的值,无法表达协议结束。
增强 for 会通过 Iterable.iterator 获得迭代器,再重复调用 hasNext 与 next。这里显式写出游标实现是为了学习协议;日常开发若已经有合适的集合迭代器,通常直接复用即可。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.iterator,源码目录为 src/main/java/com/hanserwei/patterns/iterator/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
ReadingList.java:
package com.hanserwei.patterns.iterator;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
/** 每次遍历取得独立快照的阅读列表. */
public final class ReadingList implements Iterable<String> {
/** 当前列表内容;写入仅发生在当前线程. */
private final List<String> titles = new ArrayList<>();
/** 在列表尾部添加非空标题. */
public void add(String title) {
titles.add(Objects.requireNonNull(title, "title"));
}
/** 为本次遍历创建快照和独立游标. */
@Override
public Iterator<String> iterator() {
return new SnapshotIterator(List.copyOf(titles));
}
/** 遍历快照的游标,不暴露底层列表. */
private static final class SnapshotIterator implements Iterator<String> {
/** 本次遍历冻结的标题集合. */
private final List<String> snapshot;
/** 下一个待读元素的位置. */
private int index;
/** 保存已经复制的不可变快照. */
private SnapshotIterator(List<String> snapshot) {
this.snapshot = snapshot;
}
/** 判断是否仍有待读元素,不推进游标. */
@Override
public boolean hasNext() {
return index < snapshot.size();
}
/** 返回下个元素;耗尽时遵守 Iterator 的异常契约. */
@Override
public String next() {
if (!hasNext()) {
throw new NoSuchElementException("Reading list exhausted");
}
return snapshot.get(index++);
}
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.iterator;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
ReadingList list = new ReadingList();
list.add("Java 25");
list.add("OOP");
for (String title : list) {
System.out.println(title);
}
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runIterator
./gradlew test --tests 'com.hanserwei.patterns.iterator.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
Java 25
OOP
用测试确认模式的行为
两个游标互不影响;旧快照不含后续新增标题;空列表的 next 抛规定异常,remove 抛不支持操作异常。
对应测试位于 src/test/java/com/hanserwei/patterns/iterator/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 隐藏集合的实际存储结构,同时给调用方稳定遍历协议。
- 树、分页结果或自定义容器需要独立遍历过程。
- 同一数据结构需要支持多次或不同顺序的遍历。
适用边界与容易踩的坑
快照不是免费的。每次创建迭代器都复制列表,时间和额外空间随列表长度增长;snapshot 的元素若是可变对象,也只固定引用序列,不会自动冻结元素状态。
本例没有实现 remove,沿用 Iterator 默认的不支持删除行为。它也不是 fail-fast 迭代器:创建后修改列表不会因为版本检查而抛并发修改异常,只是旧快照看不到新增项。两种契约都可以成立,但必须写清楚。
“快照遍历”不等于整个 ReadingList 线程安全。一个线程 add、另一个线程同时创建 List.copyOf,仍然缺乏同步保证。本例仅讨论同线程顺序修改与遍历。
与相近模式比较
迭代器管理如何走过对象集合;访问者管理走到一个对象之后执行什么操作。二者可以协作,例如用迭代器遍历文档元素,再对每个元素调用 accept。
动手练习
增加反向快照迭代方法,并保持常规 iterator 的顺序不变。再比较快照、实时视图和 fail-fast 三种修改可见性策略,用具体输入说明它们分别会观察到什么。
协议细节可对照 JDK 25 Iterator 文档,尤其是 next 的耗尽异常和默认 remove 行为。
系列导航:Java 25 设计模式学习指南