跳转到内容

迭代器模式 (Iterator)

用数组时 for 下标、用链表时 next 指针、用树时递归或栈——集合一换,遍历写法就换,调用方会粘在具体实现上。更麻烦的是,有时你根本不想把「底层是数组还是分页接口」暴露出去,只希望对方能按顺序拿完所有元素就行。

迭代器模式做的就是把「当前走到哪、怎么取下一个」收到一个统一接口里,集合只负责提供一个迭代器,调用方只依赖这个接口遍历,不关心底层是连续内存、链表还是每次调接口拉一页。


不同集合的存储方式不一样:数组是连续下标,链表是节点指针,树要按前序/中序走,图要按 BFS/DFS。如果在使用处针对每种结构各写一套循环,代码会充满实现细节,换一种存储就要改调用方。另一种情况是:数据本身是「分页接口」或「流式接口」,一页一页拉、或按游标拉,你希望给上层一个「像普通集合一样逐个取」的视图,而不是让上层自己管页码、拉取、拼装。

这两类需求的共同点是:遍历方式想和集合实现解耦,并且最好不暴露内部结构(不暴露数组下标、不暴露「其实在分页」)。迭代器模式的做法是:集合提供一个「迭代器」,迭代器上只有「还有没有下一个」「取下一个」之类的方法,调用方只跟迭代器打交道,遍历逻辑和游标状态都封在迭代器里。


  • 迭代器接口 (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

用数组和匿名内部类把「聚合造迭代器、调用方只认 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
}

这里底层是数组,但调用方只看到迭代器,不知道也不依赖数组。下面把「底层」换成按页拉取的数据源,迭代器内部负责翻页和游标。


很多后台要「把某张表或某个接口的数据全量扫一遍」(例如同步、统计、导出),而接口或仓库只提供分页查询。如果让业务层自己循环「查一页处理一页再查下一页」,分页逻辑和业务逻辑会缠在一起,也不好复用。用迭代器可以把「按页拉取」藏在一个迭代器里:对外只有 hasNext()next(),内部在需要时请求下一页并维护当前页内的下标。

下面用「审计日志」举例:仓库按页查,聚合是一个能创建迭代器的服务,迭代器内部持有一个「当前页 + 当前页内位置」,本页用完后自动拉下一页。

package com.example.demo.iterator;
import lombok.Data;
import java.time.Instant;
@Data
public 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;
@Data
public class PageResult<T> {
private List<T> items;
private int pageIndex;
private int pageSize;
private boolean hasMore;
}
package com.example.demo.iterator;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public 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;
}
}

直接实现 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
@RequiredArgsConstructor
public 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()),完全不知道分页存在。

例如一个「全量同步」或「全量导出」的服务:注入工厂,创建迭代器,逐个处理,不写任何分页代码。

package com.example.demo.iterator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Iterator;
@Slf4j
@Service
@RequiredArgsConstructor
public 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.IteratorIterable 就是该模式的典型应用;上面分页数据源的例子则说明,即使用户只提供「按页查」的接口,也可以通过迭代器对外呈现成「连续扫全量」,业务层代码更简单,也不暴露分页细节。