本文是【GoF设计模式】系列第19篇,更多内容欢迎关注公众号:咖啡八杯
前言
为什么需要迭代器模式?
假设需要写一个"打印集合所有元素"的方法,业务里存在三种集合:数组、自定义链表、HashMap。没有统一的遍历接口时,客户端只能这么写:
// 遍历数组:用下标
public void printArray(String[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
// 遍历自定义链表:用 next 指针
public void printLinkedList(Node head) {
Node cur = head;
while (cur != null) {
System.out.println(cur.value);
cur = cur.next;
}
}
// 遍历 HashMap:用 entrySet
public void printMap(Map<String, Integer> map) {
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}
}
问题一目了然:客户端被迫知道每种集合的内部结构——数组要靠下标、链表要靠 next 指针、HashMap 要靠 entrySet。三种集合就要写三份代码。更严重的是,一旦集合的内部结构变了(比如把数组换成红黑树),所有调用点都得改。
问题症结在于把"遍历"和"数据结构"绑死了,忽略了 "下一个元素在哪"是集合自己的事,客户端只关心"有没有下一个"和"给我下一个" 。迭代器模式解决的就是这个问题——把"游走"的动作从数据结构里剥出来,统一成一个接口,客户端从此不再关心底层是数组、链表还是树。
概念
迭代器模式(Iterator Pattern)是一种行为型设计模式,核心思想是提供一种统一的方式来顺序访问一个聚合对象中的各个元素,而不暴露该对象的内部表示。
可以把它想象成博物馆的语音导览器:无论展品是按年代排、按国别排还是按主题排,游客只需要按"下一件"按钮就能听到下一个展品的介绍——完全不需要知道展品是怎么被组织的。展馆负责摆展(内部结构),导览器负责引路(遍历接口),游客只需要跟着走。
迭代器模式涉及四个角色:
- Iterator(迭代器接口) :定义遍历元素的接口,通常包含
hasNext()判断是否还有下一个元素、next()返回当前元素并将游标后移 - ConcreteIterator(具体迭代器) :实现迭代器接口,维护当前遍历位置,实现具体的遍历逻辑
- Aggregate(聚合接口) :定义创建迭代器的接口,通常包含一个
createIterator()方法 - ConcreteAggregate(具体聚合类) :实现聚合接口,负责存储数据并返回与自身匹配的具体迭代器
classDiagram
direction BT
class Aggregate {
<<interface>>
+createIterator() Iterator
}
class ConcreteAggregate {
-elements
+createIterator() Iterator
}
class Iterator {
<<interface>>
+hasNext() boolean
+next() Object
}
class ConcreteIterator {
-index: int
+hasNext() boolean
+next() Object
}
ConcreteAggregate ..|> Aggregate : 实现
ConcreteIterator ..|> Iterator : 实现
ConcreteAggregate ..> ConcreteIterator : 创建
Aggregate ..> Iterator : 返回
图中各类之间的关系:Aggregate 接口声明 createIterator 方法,ConcreteAggregate 实现它——持有元素数据,并创建能遍历自身元素的 ConcreteIterator。Iterator 接口声明 hasNext 和 next,ConcreteIterator 用一个 index 字段维护遍历状态。客户端只依赖 Aggregate 和 Iterator 两个接口,完全不接触任何具体类。
实现
迭代器模式的核心是"游标外置"——把"当前遍历到哪儿"这个状态从聚合对象里拿出来,放到一个专门的迭代器对象里。这样同一个集合可以同时被多个迭代器遍历,互不干扰。
基础实现
定义 Iterator 接口声明 hasNext 和 next,ConcreteIterator 用一个 index 字段追踪当前位置。定义 Aggregate 接口声明 createIterator,ConcreteAggregate 内部用数组存数据并创建对应迭代器。客户端拿到迭代器后用 while (it.hasNext()) 循环遍历。
// Iterator:迭代器接口
interface Iterator {
boolean hasNext();
Object next();
}
// ConcreteIterator:具体迭代器
class ConcreteIterator implements Iterator {
private Object[] elements;
private int index; // 游标:记录当前位置
public ConcreteIterator(Object[] elements) {
this.elements = elements;
this.index = 0;
}
public boolean hasNext() {
return index < elements.length;
}
public Object next() {
if (!hasNext()) {
return null;
}
// 后置自增:先返回当前元素,再把游标后移
return elements[index++];
}
}
// Aggregate:聚合接口
interface Aggregate {
Iterator createIterator();
}
// ConcreteAggregate:具体聚合类
class ConcreteAggregate implements Aggregate {
private Object[] elements;
public ConcreteAggregate(Object[] elements) {
this.elements = elements;
}
public Iterator createIterator() {
return new ConcreteIterator(elements);
}
}
// 客户端
class Client {
public static void main(String[] args) {
String[] data = {"hello", "world", "java"};
Aggregate aggregate = new ConcreteAggregate(data);
Iterator it = aggregate.createIterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
角色对照:
- Iterator(迭代器接口) :
Iterator,声明hasNext和next - ConcreteIterator(具体迭代器) :
ConcreteIterator,用index追踪位置 - Aggregate(聚合接口) :
Aggregate,声明createIterator - ConcreteAggregate(具体聚合类) :
ConcreteAggregate,创建与自身匹配的迭代器
关键点:next 中的 index++ 是后置自增——先返回 elements[index] 再让 index = index + 1,语义上正好对应"返回当前元素并将游标后移"。客户端只依赖 Aggregate 和 Iterator 两个接口,替换集合实现(数组→链表→跳表)时客户端代码一行不改。同一个 ConcreteAggregate 可以调用 createIterator 多次,得到多个独立的迭代器互不影响——这就是"游标外置"的价值。
引入一个具体场景:订单系统需要对外提供订单遍历能力,但不希望暴露内部存储用的是数组(未来可能换成 List 或数据库)。让 OrderCollection 实现 Iterable<Order>,就能直接支持增强 for 循环。
// Element:订单
class Order {
private String orderId;
private double amount;
public Order(String orderId, double amount) {
this.orderId = orderId;
this.amount = amount;
}
public String getOrderId() { return orderId; }
public double getAmount() { return amount; }
@Override
public String toString() {
return "Order{" + orderId + ", ¥" + amount + "}";
}
}
// ConcreteAggregate:订单集合,实现 Iterable 让 for-each 直接可用
class OrderCollection implements Iterable<Order> {
private Order[] orders;
private int size;
public OrderCollection(int capacity) {
this.orders = new Order[capacity];
this.size = 0;
}
public void addOrder(Order order) {
if (size < orders.length) {
orders[size++] = order;
}
}
// 创建迭代器:返回内部类实例
public java.util.Iterator<Order> iterator() {
return new OrderIterator();
}
// ConcreteIterator:内部类迭代器
private class OrderIterator implements java.util.Iterator<Order> {
private int index = 0;
public boolean hasNext() {
return index < size;
}
public Order next() {
return orders[index++];
}
}
}
// 使用
class OrderDemo {
public static void main(String[] args) {
OrderCollection orders = new OrderCollection(10);
orders.addOrder(new Order("ORD-001", 299.0));
orders.addOrder(new Order("ORD-002", 599.0));
orders.addOrder(new Order("ORD-003", 149.5));
// 客户端不知道内部是数组,只知道能 for-each
for (Order order : orders) {
System.out.println(order);
}
}
}
角色对照:
- Iterator(迭代器接口) :
java.util.Iterator,JDK 已提供 - ConcreteIterator(具体迭代器) :
OrderIterator,作为OrderCollection的内部类 - Aggregate(聚合接口) :
java.lang.Iterable,JDK 已提供 - ConcreteAggregate(具体聚合类) :
OrderCollection
关键点:把迭代器写成内部类能直接访问外部类的 orders 和 size 字段——这是 Java 内部类的特权,比外部实现的迭代器少一次数据传递。实现 Iterable<Order> 接口后自动支持 for-each 语法,编译器会把 for (Order o : orders) 展开成基于 iterator() 的循环。客户端看到的是"能 for-each 的东西",完全不知道内部是数组。
总结
本质:把"如何遍历"从聚合对象中分离出来,用统一的接口对外提供顺序访问能力。
什么时候用:
- 需要向外暴露集合的元素但不想暴露内部结构(数组、链表、树、跳表都能用同一套接口)
- 需要为同一个集合提供多种遍历方式(前序/中序/后序、BFS/DFS)
- 需要同时进行多个独立遍历(多个游标互不干扰)
- 想让客户端能用
for-each语法遍历自定义集合
什么时候不用:
- 集合就是内部使用,客户端不需要遍历
- 只需要一次简单遍历,直接用
for-each数组/List 更清爽 - 性能极度敏感的场景——迭代器的方法调用比下标访问慢一点点
- 集合结构会在遍历过程中频繁修改,且不需要 fail-fast——这时候要单独设计并发遍历策略
简单记忆:内部结构不外露,游标外置数下一步;集合导航靠接口,for-each 只需实现 Iterable。
相似模式区分
总览
| 模式 | 核心意图 | 典型场景 |
|---|---|---|
| 迭代器 | 提供统一的遍历接口 | 集合遍历、for-each、ResultSet 封装 |
| 组合 | 将对象组合成树形结构,统一处理叶子和容器 | 菜单树、文件系统、组织架构 |
| 访问者 | 不修改元素类的前提下定义新操作 | 语法树处理、多态操作分派 |
简单记忆:组合管"怎么组织",迭代器管"怎么走过每个元素",访问者管"到了每个元素之后做什么"。
迭代器 vs 组合
两者都常见于"处理集合"的场景,但关注点截然不同:
| 维度 | 迭代器模式 | 组合模式 |
|---|---|---|
| 核心意图 | 顺序访问集合元素,隐藏遍历细节 | 将对象组合成树形结构,统一处理单元和组合 |
| 结构差异 | Iterator + Aggregate 两个接口 | Component 抽象接口 + Leaf + Composite |
| 关注点 | 怎么走过每个元素 | 怎么组织元素成层次结构 |
| 典型场景 | 遍历 List、Map、Set | 菜单树、文件系统、GUI 控件树 |
逐步区分法:
- 如果需要逐个访问集合中的元素 → 选迭代器
- 如果需要把对象组织成树形层次结构 → 选组合
- 如果既有树形结构又要遍历 → 两者结合(组合建结构,迭代器遍历节点)
简单记忆口诀:组合管"怎么组织",迭代器管"怎么遍历"。
推荐:树形菜单、文件目录这类场景通常两者一起用——用组合模式构建树,同时提供迭代器把树"拉平"成线性序列,客户端可以用 for-each 直接遍历整棵树。
迭代器 vs 访问者
两者都涉及"依次处理集合元素",但一个管路径、一个管动作:
| 维度 | 迭代器模式 | 访问者模式 |
|---|---|---|
| 核心意图 | 提供遍历路径(怎么走到每个元素) | 提供处理操作(到每个元素做什么) |
| 结构差异 | Iterator + Aggregate | Visitor + Element,双重分派 |
| 关注点 | 遍历过程 | 操作逻辑 |
| 扩展方向 | 增加新集合类型 → 加迭代器 | 增加新操作 → 加访问者 |
| 典型场景 | 集合元素的顺序访问 | 抽象语法树、编译器、复杂对象结构 |
逐步区分法:
- 如果只需要逐个拿到元素做简单处理 → 选迭代器
- 如果需要在不修改元素类的前提下定义多种新操作 → 选访问者
- 如果元素类型多样,且不同类型要执行不同逻辑 → 选访问者(利用双重分派)
简单记忆口诀:迭代器管"怎么走到每个元素",访问者管"到了之后做什么"。
推荐:单纯遍历用迭代器就够了,只有当"多种类型 × 多种操作"的矩阵扩展成为瓶颈时才需要访问者。90% 的业务场景迭代器足以。
练习题目
学生名单
题目描述:小明是一位老师,在进行班级点名时,希望有一个学生名单系统,请实现迭代器模式提供一个迭代器使得可以按顺序遍历学生列表。
输入描述:第一行是一个整数 N(1 <= N <= 100),表示学生的数量。接下来的 N 行,每行包含一个学生的信息,格式为 姓名 学号。
输出描述:输出班级点名的结果,即按顺序遍历学生列表,输出学生的姓名和学号。
输入示例:
3
Alice 1001
Bob 1002
Charlie 1003
输出示例:
Alice 1001
Bob 1002
Charlie 1003
解题思路:题目要求实现标准的迭代器模式——遍历路径由迭代器负责,客户端只调用 hasNext 和 next。
角色对应:
- Iterator(迭代器接口) :
MyIterator,声明hasNext和next - ConcreteIterator(具体迭代器) :
StudentIterator,用index追踪位置 - Aggregate(聚合接口) :
MyIterable,声明createIterator - ConcreteAggregate(具体聚合类) :
StudentSystem,持有学生数组
核心逻辑:
- 输入完 N 个学生后构造
StudentSystem,通过createIterator拿到迭代器 - 迭代器内部用
index++后置自增——先返回当前学生再后移游标 - 客户端用
while (it.hasNext())循环,完全不接触底层数组
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
Student[] students = new Student[n];
for (int i = 0; i < n; i++) {
String name = sc.next();
String number = sc.next();
students[i] = new Student(name, number);
}
StudentSystem ss = new StudentSystem(students);
MyIterator it = ss.createIterator();
while (it.hasNext()) {
Student s = it.next();
s.show();
}
}
}
// Iterator:迭代器接口
interface MyIterator {
boolean hasNext();
Student next();
}
// ConcreteIterator:具体迭代器
class StudentIterator implements MyIterator {
private Student[] students;
private int index;
public StudentIterator(Student[] students) {
this.students = students;
this.index = 0;
}
public boolean hasNext() {
return index < students.length;
}
public Student next() {
if (hasNext()) {
return students[index++]; // 后置自增:先返回再后移
}
return null;
}
}
// Aggregate:聚合接口
interface MyIterable {
MyIterator createIterator();
}
// ConcreteAggregate:具体聚合类
class StudentSystem implements MyIterable {
private Student[] students;
public StudentSystem(Student[] students) {
this.students = students;
}
public MyIterator createIterator() {
return new StudentIterator(students);
}
}
// 元素类
class Student {
private String name;
private String number;
public Student(String name, String number) {
this.name = name;
this.number = number;
}
public void show() {
System.out.println(name + " " + number);
}
}
扩展:实际项目中的迭代器模式
Java 集合框架的 Iterator
Java 集合框架是迭代器模式的教科书级实现。所有集合类(ArrayList、LinkedList、HashSet、HashMap.entrySet 等)都实现了 Iterable 接口,返回 Iterator 对象。写过 Java 的人几乎每天都在用它——但很多人没意识到自己在用一个 GoF 模式。增强 for 循环(for-each)本质上就是编译器帮忙展开的迭代器遍历,实现 Iterable 接口的自定义类可以直接用 for-each 语法。
List<String> list = new ArrayList<>(List.of("Java", "Python", "Go"));
// 显式使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
// for-each 语法:编译器展开为迭代器循环
for (String lang : list) {
System.out.println(lang);
}
💡 想看得更透,不妨打开 JDK 源码翻两个内部类:ArrayList.Itr 正是维护 cursor、lastRet 游标并实现 fail-fast 的那个私有内部类;LinkedList.ListItr 还实现了 ListIterator,额外支持 hasPrevious()、previous() 双向遍历与 set()、add() 修改元素。一个只往前走,一个能来回--迭代器在 JDK 里早已玩出花。
fail-fast 机制与安全删除
Java 集合的迭代器在遍历时如果发现集合被外部修改就会立刻抛出 ConcurrentModificationException——这就是 fail-fast 机制。原理是迭代器记录了创建时的 modCount,每次 next() 都会比对:如果直接 list.remove(item) 会改变 modCount,迭代器立刻发现不一致就报错。这个机制不属于 GoF 迭代器模式的核心,是 Java 集合额外加的安全网,但初学者常在这里踩坑。正确的删除方式是使用迭代器自己的 remove(),它会同步更新 expectedModCount。
List<String> list = new ArrayList<>(List.of("Java", "Python", "Go"));
// 错误:抛 ConcurrentModificationException
for (String item : list) {
if ("Python".equals(item)) {
list.remove(item); // 破坏 modCount
}
}
// 正确:用迭代器自己的 remove,同步 expectedModCount
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("Python".equals(it.next())) {
it.remove(); // 安全删除
}
}
// Java 8+ 简洁写法
list.removeIf(item -> "Python".equals(item));
Stream API 与内部迭代器
前面所有写法都是"外部迭代器"--客户端自己控制 while (it.hasNext()) 循环,决定什么时候取下一个、怎么处理。Java 8 的 Stream API 把这套翻了过来:集合自己负责遍历,调用方只声明"要做什么",这就是内部迭代器。控制权从客户端反转回集合,从"怎么遍历"跃迁到"遍历什么"。
List<String> langs = List.of("Java", "Python", "Go");
// 外部迭代器:客户端控制循环,自己管 hasNext/next
Iterator<String> it = langs.iterator();
while (it.hasNext()) {
String lang = it.next();
if (lang.length() > 3) System.out.println(lang);
}
// 内部迭代器:集合自己遍历,调用方只声明动作
langs.stream()
.filter(lang -> lang.length() > 3)
.forEach(System.out::println);
外部迭代器:循环、取值、判断全写在客户端,集合只被动提供 hasNext/next。内部迭代器:filter、forEach 把行为传进集合,集合自己决定怎么走完每个元素。这一层反转带来的不只是写法简洁--并行化变得可能(parallelStream 把"怎么遍历"交给运行时,客户端代码一行不改),惰性求值也让中间操作可以合并执行。迭代器模式解决"如何统一遍历",Stream 在它之上多回答了一个问题:"遍历时如何把行为也统一起来"。
文件目录递归遍历
目录是天然的树形结构,用迭代器可以把递归遍历的复杂度屏蔽掉——客户端拿到的是一个线性序列。用 Queue 实现 BFS 按层级访问,改成 Stack 就是 DFS。这种把"遍历策略"和"访问动作"解耦的做法,让业务代码专注在"对每个文件做什么",而不是"怎么走到每个文件"。JDK 7 的 Files.walk 本质就是这种思路的现代版。
class FileIterator implements Iterator<File> {
private Queue<File> queue = new LinkedList<>();
public FileIterator(File root) {
if (root.exists()) queue.offer(root);
}
public boolean hasNext() { return !queue.isEmpty(); }
public File next() {
File current = queue.poll();
if (current != null && current.isDirectory()) {
File[] children = current.listFiles();
if (children != null) {
for (File child : children) queue.offer(child);
}
}
return current;
}
}
// 客户端只需 while(it.hasNext()) 即可遍历整棵目录树
树形菜单展平
后台管理系统里菜单几乎都是树形结构——但渲染、权限检查这些操作往往需要"平铺"访问每个节点。迭代器可以把树形结构展平成线性序列,客户端用 for-each 就能扫过所有节点,不用手写递归。用 Stack 实现深度优先,用 Queue 换成广度优先,遍历策略切换只在迭代器内部。这个套路在 GUI 控件树、组织架构树、评论回复树里都能直接套用。
class MenuIterator implements Iterator<MenuItem> {
private Deque<MenuItem> stack = new ArrayDeque<>();
public MenuIterator(MenuItem root) {
pushChildren(root); // 从根节点的子节点开始
}
private void pushChildren(MenuItem item) {
// 逆序压栈,保证按原始顺序弹出
List<MenuItem> children = item.getChildren();
for (int i = children.size() - 1; i >= 0; i--) {
stack.push(children.get(i));
}
}
public boolean hasNext() { return !stack.isEmpty(); }
public MenuItem next() {
MenuItem current = stack.pop();
pushChildren(current); // DFS:立刻深入子节点
return current;
}
}
组合模式与迭代器模式联动:菜单权限检查
前面"树形菜单展平"只看到迭代器怎么把树拉平,但那棵树是怎么搭起来的?这就轮到组合模式登场了。MenuItem 既是叶子也是容器,add 把子菜单挂上来递归建出整棵树;迭代器负责把树展平成线性序列;权限校验只需对每个节点问一句"当前用户有没有这个权限码"。三步各司其职:组合管结构,迭代器管路径,业务管动作。
// 组合模式:统一叶子和容器
class MenuItem {
private String code; // 权限码
private List<MenuItem> children = new ArrayList<>();
public void add(MenuItem child) { children.add(child); }
// 构造、getChildren、getCode 省略
}
// 组合建树 -> 迭代器展平 -> 逐节点检查权限
MenuItem root = buildMenuTree(); // 组合模式搭建树形菜单
MenuIterator it = new MenuIterator(root); // 迭代器把树展平成线性序列
Set<String> userPerms = currentUserPerms(); // 当前用户拥有的权限码
while (it.hasNext()) { // 逐节点检查
MenuItem item = it.next();
if (!userPerms.contains(item.getCode())) {
// 无权限:从渲染树里剔除该节点
}
}
迭代器复用前面 MenuIterator 的 DFS 展平逻辑,组合模式只管"怎么搭树",两者衔接代价为零--这也是树形结构场景几乎总是这两个模式成对出现的原因。权限过滤、按钮树渲染、组织架构审批,都是同一套组合拳。
JDBC ResultSet 封装成标准迭代器
JDBC 的 ResultSet 本身就是迭代器的一个变体——rs.next() 移动游标、rs.getXxx() 取列值。但 API 不够简洁:受检异常、手动关闭、无法用 for-each。把 ResultSet 包成实现了 Iterator<T> 的类之后,客户端就能用增强 for 循环直接消费数据库结果。Spring 的 JdbcTemplate.queryForStream 就采用了类似思路——用迭代器把懒加载的结果集串成 Java 8 的 Stream。
class ResultSetIterator<T> implements Iterator<T> {
private ResultSet rs;
private RowMapper<T> mapper;
private boolean hasNextResult;
public ResultSetIterator(ResultSet rs, RowMapper<T> mapper) throws SQLException {
this.rs = rs;
this.mapper = mapper;
this.hasNextResult = rs.next(); // 预取第一行
}
public boolean hasNext() { return hasNextResult; }
public T next() {
try {
T item = mapper.map(rs);
hasNextResult = rs.next(); // 预取下一行
return item;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
分页查询的迭代器思想
后端开发里分页查询是家常便饭,但很少有人把它和迭代器联系起来。仔细一想,分页就是迭代器的变体:hasNextPage 对应 hasNext,getNextPage 对应 next,游标从"元素下标"变成了"页码"。把分页器抽象成迭代器后,调用方不用关心是查数据库还是调第三方接口,统一用 while (pager.hasNext()) 拉取,"翻页"这个动作被彻底屏蔽。
class PageIterator<T> implements Iterator<List<T>> {
private int page = 1; // 游标:当前页码
private int size;
private PageFetcher<T> fetcher;
private List<T> nextBatch; // 预取下一页
public boolean hasNext() { return !nextBatch.isEmpty(); }
public List<T> next() {
List<T> current = nextBatch;
page++;
nextBatch = fetcher.fetch(page, size); // 拉取下一页
return current;
}
}
// 大表同步、增量拉取、对接第三方分页接口,都能套这个壳
PageHelper 这类插件拦截 SQL 自动拼 LIMIT,本质也是替调用方管理这个"页码游标"。把分页想成迭代器,API 设计就能借用现成的模式语言:hasNext 表达"还有没有下一批",next 表达"给我下一批",读起来一目了然。
技术交流 & 更多原创内容,关注公众号:咖啡八杯