迭代器模式 (Iterator)
用数组时 for 下标、用链表时 next 指针、用树时递归或栈——集合一换,遍历写法就换,调用方会粘在具体实现上。更麻烦的是,有时你根本不想把「底层是数组还是分页接口」暴露出去,只希望对方能按顺序拿完所有元素就行。
迭代器模式做的就是把「当前走到哪、怎么取下一个」收到一个统一接口里,集合只负责提供一个迭代器,调用方只依赖这个接口遍历,不关心底层是连续内存、链表还是每次调接口拉一页。
一、问题从哪来?
Section titled “一、问题从哪来?”不同集合的存储方式不一样:数组是连续下标,链表是节点指针,树要按前序/中序走,图要按 BFS/DFS。如果在使用处针对每种结构各写一套循环,代码会充满实现细节,换一种存储就要改调用方。另一种情况是:数据本身是「分页接口」或「流式接口」,一页一页拉、或按游标拉,你希望给上层一个「像普通集合一样逐个取」的视图,而不是让上层自己管页码、拉取、拼装。
这两类需求的共同点是:遍历方式想和集合实现解耦,并且最好不暴露内部结构(不暴露数组下标、不暴露「其实在分页」)。迭代器模式的做法是:集合提供一个「迭代器」,迭代器上只有「还有没有下一个」「取下一个」之类的方法,调用方只跟迭代器打交道,遍历逻辑和游标状态都封在迭代器里。
二、几个角色
Section titled “二、几个角色”- 迭代器接口 (Iterator):抽象「逐个访问」的协议,常见有
hasNext()和next(),有的还会加remove()。调用方只依赖这个接口。 - 具体迭代器 (ConcreteIterator):针对某一种聚合实现上述接口,内部持有聚合的引用以及「当前走到哪」的游标(或等价状态),
next()时按该聚合的存储方式取下一个元素。 - 聚合 (Aggregate):即「集合」的抽象,至少提供一个方法(如
createIterator())返回一个迭代器。有的语言里聚合自己实现Iterable,用iterator()返回迭代器。 - 具体聚合 (ConcreteAggregate):具体的集合类,实现
createIterator(),返回能遍历自己的那种迭代器。
客户端拿到聚合后,通过 createIterator() 拿到迭代器,然后用 while (it.hasNext()) ... it.next() 遍历,不再依赖数组、链表或分页细节。
聚合负责「造迭代器」,迭代器负责「走」并依赖具体聚合取数。
classDiagram
class Iterator {
<<interface>>
+hasNext()* boolean
+next()* Object
}
class ConcreteIterator {
-aggregate ConcreteAggregate
-cursor int
+hasNext() boolean
+next() Object
}
class Aggregate {
<<interface>>
+createIterator()* Iterator
}
class ConcreteAggregate {
+createIterator() Iterator
}
Iterator <|.. ConcreteIterator
Aggregate <|.. ConcreteAggregate
ConcreteIterator o-- ConcreteAggregate
四、示例:数组 + 匿名迭代器
Section titled “四、示例:数组 + 匿名迭代器”用数组和匿名内部类把「聚合造迭代器、调用方只认 hasNext/next」。
public interface Iterator<T> { boolean hasNext(); T next();}
public class ConcreteAggregate { private final String[] items = {"A", "B", "C"};
public Iterator<String> createIterator() { return new Iterator<String>() { private int index = 0; @Override public boolean hasNext() { return index < items.length; } @Override public String next() { return items[index++]; } }; }}
// 调用方ConcreteAggregate agg = new ConcreteAggregate();Iterator<String> it = agg.createIterator();while (it.hasNext()) { System.out.println(it.next()); // A, B, C}这里底层是数组,但调用方只看到迭代器,不知道也不依赖数组。下面把「底层」换成按页拉取的数据源,迭代器内部负责翻页和游标。
五、分页数据源的迭代器
Section titled “五、分页数据源的迭代器”很多后台要「把某张表或某个接口的数据全量扫一遍」(例如同步、统计、导出),而接口或仓库只提供分页查询。如果让业务层自己循环「查一页处理一页再查下一页」,分页逻辑和业务逻辑会缠在一起,也不好复用。用迭代器可以把「按页拉取」藏在一个迭代器里:对外只有 hasNext() 和 next(),内部在需要时请求下一页并维护当前页内的下标。
下面用「审计日志」举例:仓库按页查,聚合是一个能创建迭代器的服务,迭代器内部持有一个「当前页 + 当前页内位置」,本页用完后自动拉下一页。
5.1 实体与分页结果
Section titled “5.1 实体与分页结果”package com.example.demo.iterator;
import lombok.Data;import java.time.Instant;
@Datapublic class AuditEntry { private Long id; private String action; private String operator; private Instant createdAt;}package com.example.demo.iterator;
import lombok.Data;import java.util.List;
@Datapublic class PageResult<T> { private List<T> items; private int pageIndex; private int pageSize; private boolean hasMore;}5.2 按页查的仓库(数据来源)
Section titled “5.2 按页查的仓库(数据来源)”package com.example.demo.iterator;
import org.springframework.stereotype.Repository;import java.util.ArrayList;import java.util.List;
@Repositorypublic class AuditRepository { public PageResult<AuditEntry> findPage(int pageIndex, int pageSize) { // 实际从 DB 或远程接口分页查询,这里用假数据示意 List<AuditEntry> items = new ArrayList<>(); int from = pageIndex * pageSize; int to = Math.min(from + pageSize, 100); for (int i = from; i < to; i++) { AuditEntry e = new AuditEntry(); e.setId((long) i); e.setAction("action-" + i); e.setOperator("op"); e.setCreatedAt(java.time.Instant.now()); items.add(e); } PageResult<AuditEntry> r = new PageResult<>(); r.setItems(items); r.setPageIndex(pageIndex); r.setPageSize(pageSize); r.setHasMore(to < 100); return r; }}5.3 分页迭代器实现
Section titled “5.3 分页迭代器实现”直接实现 java.util.Iterator<AuditEntry>:内部维护当前页、当前页内下标,本页用尽时拉下一页。
package com.example.demo.iterator;
import lombok.RequiredArgsConstructor;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import java.util.Iterator;import java.util.List;import java.util.NoSuchElementException;
@Component@RequiredArgsConstructorpublic class AuditLogIteratorFactory { private final AuditRepository auditRepository; @Value("${app.audit.page-size:20}") private int pageSize;
public Iterator<AuditEntry> createIterator() { return new Iterator<AuditEntry>() { private int pageIndex = 0; private List<AuditEntry> currentPage = null; private int indexInPage = 0;
private void ensurePage() { while ((currentPage == null || indexInPage >= currentPage.size())) { PageResult<AuditEntry> result = auditRepository.findPage(pageIndex, pageSize); currentPage = result.getItems(); indexInPage = 0; if (currentPage.isEmpty() && !result.isHasMore()) break; if (!result.isHasMore() && currentPage.isEmpty()) break; if (!currentPage.isEmpty()) return; pageIndex++; } }
@Override public boolean hasNext() { ensurePage(); return currentPage != null && indexInPage < currentPage.size(); }
@Override public AuditEntry next() { if (!hasNext()) throw new NoSuchElementException(); AuditEntry e = currentPage.get(indexInPage++); return e; } }; }}「聚合」在这里是 AuditLogIteratorFactory:它依赖仓库和分页大小,createIterator() 返回一个迭代器,迭代器内部按需拉页并推进游标。调用方拿到的是 Iterator<AuditEntry>,只做 while (it.hasNext()) process(it.next()),完全不知道分页存在。
5.4 在服务或接口里用
Section titled “5.4 在服务或接口里用”例如一个「全量同步」或「全量导出」的服务:注入工厂,创建迭代器,逐个处理,不写任何分页代码。
package com.example.demo.iterator;
import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import java.util.Iterator;
@Slf4j@Service@RequiredArgsConstructorpublic class AuditSyncService { private final AuditLogIteratorFactory iteratorFactory;
public int syncAll() { Iterator<AuditEntry> it = iteratorFactory.createIterator(); int count = 0; while (it.hasNext()) { AuditEntry entry = it.next(); // 同步到别处、写文件、发消息等 count++; } return count; }}若需要从接口暴露「流式导出」,也可以在 Controller 里拿到同一个迭代器,按需写响应体,逻辑仍然是一次 createIterator() 然后循环 next()。
迭代器把「如何遍历、当前走到哪」从集合里抽出来,放进单独的迭代器对象,调用方只依赖 hasNext() / next(),不依赖底层是数组、链表还是分页接口。聚合只负责提供迭代器,符合单一职责。Java 自带的 java.util.Iterator 和 Iterable 就是该模式的典型应用;上面分页数据源的例子则说明,即使用户只提供「按页查」的接口,也可以通过迭代器对外呈现成「连续扫全量」,业务层代码更简单,也不暴露分页细节。