配套代码: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 设计模式学习指南

上一篇:解释器 · 下一篇:中介者