老读者可能记得,我隔一段时间就会把23种设计模式拿出来重新过一遍。原因很简单:设计模式这玩意,背概念是最没用的,但你要是真正吃透了一个模式,写代码的思路会通透一大截。这次轮到迭代器模式,听名字好像有点枯燥,但你要是写过Java里的for (String s : list),或者用过C++里std::vector<int>::iterator那种写法,那你其实早就和它打过交道了。
这篇我用讲人话的方式,把它从原理到代码、从坑点到隐藏应用完整拆一遍。特别适合准备设计模式期末、赶设计模式大作业的人,也适合那些看完概念但不知道怎么在真实项目里落地的朋友。另外结合最近圈子里的新动静——很多现代框架在做“主从模式”调度时,其实就是在借迭代器这套思路管理任务序列,我会在最后一节重点聊聊这个场景。
1. 迭代器模式到底解决了什么问题
1.1 先从一个生活化的场景入手
假设你开了一家小书店,书架上有各种书。你请了个店员,让他把所有书名报一遍。现在问题来了:书架的摆放方式可能随时变——今天是数组排成一排,明天可能改成按类别分到多个格子里,后天可能直接在数据库里存着。
如果店员每次都要问“书架是怎么摆的,我好写代码去遍历”,那书架一改,店员就得跟着改。这显然不合理。更合理的做法是:书店统一发一个“顺序器”,店员只需要拿着这个顺序器说“下一本、下一本”,完全不用关心底层书架是数组还是数据库。
这个“顺序器”就是迭代器。它把“怎么遍历”这件事,从“业务代码”里彻底剥离开来。
1.2 迭代器模式的核心角色
迭代器模式在GoF书里的定义很简洁:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
拆解开就是四个角色:
- Iterator(迭代器接口):定义访问和遍历元素的接口,通常有
next()、hasNext()这类方法。 - ConcreteIterator(具体迭代器):实现迭代器接口,内部记录当前遍历位置。
- Aggregate(聚合接口):定义创建迭代器的方法,比如
createIterator(),对应Java里的Iterable接口。 - ConcreteAggregate(具体聚合类):实现聚合接口,返回一个具体的迭代器实例。
一句话概括:集合负责存数据,迭代器负责取数据,两者通过统一的接口协作。业务方只需要面向迭代器编程,永远不必知道集合内部到底用的什么数据结构。
1.3 没有迭代器的时候我们怎么写遍历
为了理解它的价值,我带你回退到“石器时代”。假如你直接用数组存数据,遍历就是:
java复制Book[] books = new Book[10];
for (int i = 0; i < books.length; i++) {
System.out.println(books[i].getName());
}
一切安好。但某天需求变了,书架改成了ArrayList,你的遍历代码就得改成:
java复制List<Book> books = new ArrayList<>();
for (int i = 0; i < books.size(); i++) {
System.out.println(books.get(i).getName());
}
再后来,书架换成Set,LinkedList,或者你自己写的一个树结构,每换一次,所有调用方都要跟着改一遍。更痛苦的是,有些数据结构根本不支持随机访问(比如链表),你不能通过下标拿到元素,这时候还得先搞清楚它内部怎么组织的。
迭代器模式就是为这个问题而生的。你只要保证集合能返回一个迭代器,调用方就永远只写一套遍历逻辑。这也正好符合设计模式的核心思想:封装变化,面向接口编程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 设计思路拆解与方案取舍
2.1 为什么要把“遍历”单独抽出来
很多人第一次学迭代器会觉得多此一举:我用for循环不是挺好吗?为什么要绕一圈搞个迭代器?
关键在于“遍历”和“存储”是两类不同的关注点。存储你可以用数组、链表、树、哈希表,各有各的优劣;但遍历的逻辑,对调用方来说应该是一致的——无非就是“有没有下一个”“给我下一个”这两件事。
把遍历单独抽出来以后,至少带来三个立竿见影的好处:
- 调用方解耦:业务代码不依赖具体集合类型,换集合不换逻辑。
- 集合内部自由度提升:集合类可以随意调整内部数据结构,只要迭代器接口不变,不会影响任何外部代码。
- 遍历逻辑可以复用和扩展:你可以为同一个集合写多个不同的迭代器,比如正序遍历、倒序遍历、过滤掉某些元素的遍历,而不用去动集合本身。
2.2 内部迭代器与外部迭代器的区别
迭代器还有两种风格,很多人容易忽略:
- 外部迭代器(External Iterator):由客户端控制迭代流程,也就是我们常写的
while (iterator.hasNext()) { iterator.next(); }。客户端主动拉取元素,这是最常见的写法,Java的Iterator就是这种。 - 内部迭代器(Internal Iterator):迭代流程由集合自身控制,客户端只需要传入一个回调函数,集合内部自己遍历完然后逐个回调。例如Java 8里的
list.forEach(item -> {...}),以及C++标准库里的std::for_each。
外部迭代器更灵活,可以在遍历过程中做更复杂的控制,比如手动跳过、提前终止、嵌套遍历;内部迭代器更简洁,代码可读性好,适合“对每个元素执行同一操作”的场景。
实际项目里两者经常混用。比如你用Spring Data JPA查询出一个列表,简单打印时forEach一把梭,但需要边遍历边修改数据时,还是会老老实实拿Iterator手动控制。
2.3 迭代器模式与相关模式的关系
学设计模式最忌讳把每个模式当成孤岛。迭代器模式经常和下面几个模式一起出现:
- 工厂方法模式:
iterator()方法本质上就是一个工厂方法,由它生产具体的迭代器对象。所以你会看到很多集合类的createIterator()内部返回的是匿名内部类或独立迭代器类。 - 组合模式:树形结构(如文件目录、组织架构)经常用组合模式组织节点,而树的遍历(前序、中序、后序)非常适合用迭代器封装。你只要给组合结构提供一个迭代器,外部就能用统一方式遍历整棵树。
- 责任链模式:两者都有“沿着一条链向后走”的意味,但目的不同。责任链是“把请求沿着链传递,直到有人处理”,迭代器是“沿着结构依次取出元素”。有些复杂场景里,你可以用迭代器遍历一条责任链上的处理器,逐个执行。
理解这些关联以后,你再看其他模式,会发现它们并不是23个孤立的知识点,而是一张可以互相组合的关系网。这一点在准备期末和大作业的时候特别重要,老师很喜欢问“这个模式还能和哪个模式配合”。
3. 核心实现:Java和C++两种风格,可直接抄作业
3.1 Java手写迭代器:一个完整可运行示例
Java的java.util.Iterator和java.lang.Iterable是语言层面的标准接口,所以手写迭代器非常顺手。我以书架和书为例,写一个最典型的实现。
先定义Book,就是一个简单的实体类:
java复制public class Book {
private String name;
public Book(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
然后是聚合类BookShelf。注意这里我实现了Iterable<Book>接口,这意味着它必须实现iterator()方法:
java复制import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class BookShelf implements Iterable<Book> {
private List<Book> books;
public BookShelf() {
this.books = new ArrayList<>();
}
public void addBook(Book book) {
books.add(book);
}
public int size() {
return books.size();
}
public Book getBookAt(int index) {
return books.get(index);
}
@Override
public Iterator<Book> iterator() {
return new BookShelfIterator(this);
}
}
关键来了,具体迭代器BookShelfIterator。它相当于一个“书签”,记录当前遍历到哪个位置:
java复制import java.util.Iterator;
public class BookShelfIterator implements Iterator<Book> {
private BookShelf bookShelf;
private int index;
public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
this.index = 0;
}
@Override
public boolean hasNext() {
return index < bookShelf.size();
}
@Override
public Book next() {
return bookShelf.getBookAt(index++);
}
}
测试一下:
java复制public class Main {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf();
bookShelf.addBook(new Book("设计模式"));
bookShelf.addBook(new Book("重构"));
bookShelf.addBook(new Book("代码整洁之道"));
// 使用增强for循环,底层本质上就是在使用迭代器
for (Book book : bookShelf) {
System.out.println(book.getName());
}
}
}
这段代码跑起来会依次打印三本书名。注意,for (Book book : bookShelf)能成立,唯一的前提就是BookShelf实现了Iterable。这就是迭代器模式在Java里最核心的价值——它是语言语法层面的基础支撑。
3.2 C++手写迭代器:体验STL风格
C++的做法和Java有点不一样。C++的迭代器不是通过接口实现的,而是通过运算符重载,让自定义类表现得像指针一样。这也是很多C++新手学迭代器时觉得别扭的地方。
我用一个最简单的动态数组模板来演示:
cpp复制#include <iostream>
template<typename T>
class MyVector {
public:
MyVector() : data(nullptr), size(0), capacity(0) {}
~MyVector() {
delete[] data;
}
void push_back(const T& value) {
if (size == capacity) {
capacity = capacity == 0 ? 1 : capacity * 2;
T* newData = new T[capacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
delete[] data;
data = newData;
}
data[size++] = value;
}
// 迭代器类
class Iterator {
public:
Iterator(T* ptr) : ptr(ptr) {}
T& operator*() const {
return *ptr;
}
Iterator& operator++() {
ptr++;
return *this;
}
bool operator!=(const Iterator& other) const {
return ptr != other.ptr;
}
private:
T* ptr;
};
Iterator begin() {
return Iterator(data);
}
Iterator end() {
return Iterator(data + size);
}
private:
T* data;
int size;
int capacity;
};
主函数里可以这样用:
cpp复制int main() {
MyVector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
for (MyVector<int>::Iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << std::endl;
}
return 0;
}
这个实现里最核心的是四个操作:operator*(取当前元素)、operator++(前进到下一个元素)、operator!=(判断是否走到末尾)、begin()和end()(获取首尾边界)。你会发现,C++ STL里所有容器的迭代器,本质上都是这套逻辑的封装,只是人家做成了统一的规范。
C++迭代器和Java迭代器一个显著区别是:Java返回boolean的hasNext()和返回元素的next()分开,C++则用begin()/end()来表示遍历区间,遍历中通过it != end()判断是否结束。两种风格各有千秋,理解其一,另一个自然通。
3.3 实现迭代器必知的3个细节
写迭代器不是写完能跑就算完事,有几个细节我建议你留意。
第一个是快速失败机制。Java里很多容器的迭代器都有modCount校验,一旦在迭代过程中容器被结构性修改(比如增删元素),再调用next()就会抛出ConcurrentModificationException。这其实是设计上的“防御性妥协”——为了保证迭代结果的一致性,宁可打断你也不给你不确定的行为。
第二个是迭代器的“快照”问题。有些迭代器(比如Java的CopyOnWriteArrayList的迭代器)是在创建时固定了一份数据快照,之后原集合怎么改都不影响迭代器。这种设计适合读多写少的并发场景,代价是内存开销变大。
第三个是空集合和越界。C++里如果集合为空,begin()返回end(),直接不进入循环,这是很自然的。但用Java时你要格外注意:调用iterator().next()前必须先hasNext(),否则会抛NoSuchElementException。这些边界情况在高并发、复杂业务里最容易暴露。
3.4 生产环境别造轮子,直接用现成的
说实话,真实项目里你几乎不会手写迭代器,因为Java集合框架和C++ STL已经给你提供了非常完整的实现。
Java这边,List、Set、Queue甚至Map的keySet()、values()、entrySet()返回的集合,全部支持迭代。你写for (Map.Entry<K, V> entry : map.entrySet()),底层的entrySet()集合就提供了一个迭代器。
C++这边,STL六种迭代器类型(输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器、连续迭代器)定义得非常精细。std::vector支持随机访问迭代器,所以你可以it + 5这样跳着访问;std::list只支持双向迭代器,就不能直接it + 5,必须std::advance(it, 5)。
理解了手写迭代器的原理,你才能真正理解STL容器之间迭代器能力差异的原因——不是标准库故意搞区别,而是底层数据结构决定了你能提供哪些能力。数组天然支持随机访问,链表就不行,这是物理规律决定的。
4. 真实场景:迭代器在项目里到底怎么发光
4.1 数据库游标:最像迭代器的老朋友
如果你写过数据库查询,你一定用过游标(Cursor)。JDBC里ResultSet的用法,本质上就是迭代器模式:
java复制ResultSet rs = statement.executeQuery("SELECT * FROM books");
while (rs.next()) {
String name = rs.getString("name");
System.out.println(name);
}
next()移动游标到下一行,配合getString()等取值方法。数据库把查询结果一车一车从服务端拉回来,客户端完全不用关心结果集是怎么存储、怎么分片的,只需要不断问“还有没有下一行”。你要是理解了迭代器模式,再看ResultSet会瞬间通透。
4.2 树形结构遍历:一个集合,多种迭代器
树是迭代器模式最能体现价值的数据结构之一。比如组织机构树,你要正序遍历、倒序遍历、按层级遍历,如果不使用迭代器,调用方就得自己写递归,而且写出来的代码没法复用。
实现思路是这样的:聚合类是Tree,它提供一个iterator()方法,返回一个内部封装了栈或队列的TreeIterator。这个迭代器维护遍历状态,每次next()沿着栈往下走。外部调用方完全看不到递归和栈的操作。
好处是你可以给同一棵树写多个迭代器:前序遍历迭代器、层序遍历迭代器、过滤掉禁用节点的迭代器。调用方想用哪种就传哪种,树的内部结构一点不用改。
4.3 分页查询和数据流处理
分页查询大家肯定不陌生:从数据库拿到某页数据,展示给用户。迭代器模式在数据流处理里也特别常见。比如你用Java的Stream做数据管道:
java复制List<Book> result = books.stream()
.filter(book -> book.getName().startsWith("设计"))
.map(Book::getName)
.collect(Collectors.toList());
stream()方法返回的Stream,其内部就利用了迭代器配合函数式接口,让数据像流水线一样经过一道道工序。你不需要写循环、不需要管中间临时变量,这正是迭代器“把遍历控制权交给框架”的高级用法。
4.4 现代智能体框架中的“另类迭代器”
最近圈内讨论比较多的一个话题,是关于多智能体(Multi-Agent)的设计模式。很多人在聊主从模式,也就是用主Agent统一调度多个子Agent。我在工程里读了一些方案的源码,发现一个很有意思的点:在实现主Agent调度子Agent的任务队列时,本质上就是把subagent当作一种特殊工具去调用,用主循环依次遍历任务列表、逐个执行。
你仔细看,这背后的遍历逻辑正是迭代器模式的思路。主Agent维护一个任务序列,无论这个序列是数组、队列还是动态生成的目标列表,主循环都是反复执行“判断还有没有下一个任务”和“取出下一个任务并执行”这两步。如果底层的任务存储结构变了,只要迭代逻辑不变,主Agent的调度代码就完全不用改。
这种“把执行单元当工具,用统一接口遍历调用”的思想,和传统迭代器模式的“不暴露集合内部结构”如出一辙。所以我常说,设计模式这东西不会过时,它会换一身马甲,出现在新的架构范式里。
5. 常见问题与避坑指南
5.1 遍历时删除元素:最大的坑
我见过无数新人在遍历List的时候用下面的写法:
java复制for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("bad")) {
list.remove(i);
}
}
这个写法在多数情况下会出问题。因为删除元素后,后续元素下标前移,你的i却还在自增,导致跳过一些元素。正确姿势是使用迭代器自己的remove()方法:
java复制Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if (item.equals("bad")) {
it.remove();
}
}
或者Java 8之后也可以用removeIf:
java复制list.removeIf(item -> item.equals("bad"));
在C++里面更是如此,在遍历std::vector时直接erase某个迭代器,会导致该迭代器及其后续迭代器全部失效。正确做法是it = vec.erase(it);,让erase返回下一个有效迭代器。
我个人的忠告:尽量不要在迭代过程中修改集合结构,如果必须改,优先使用迭代器自带的remove方法。 这是迭代器应用场景里最典型的坑,面试也常考。
5.2 迭代器失效:C++老生常谈的痛
C++里vector的迭代器失效,主要发生在扩容和插入删除时。比如:
cpp复制std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.push_back(4); // 如果扩容了,it就失效了
std::cout << *it; // 未定义行为
原因在于vector扩容时会把所有元素拷贝到新的内存空间,旧内存上的迭代器“指”的东西已经不存在了。避免方式有几种:提前reserve预留容量、用下标代替迭代器、或者选用std::list这类迭代器稳定性更好的容器。
Java里vector扩容也是同一个道理吗?不是。Java的ArrayList扩容后,你持有的迭代器依然存在,但如果你在迭代过程中通过集合本身的add或remove修改了结构,就会触发快速失败异常。这也是两种语言在使用习惯上的一大差异,别把一套经验生搬硬套到另一个语言里。
5.3 迭代器性能问题:别盲目追求“优雅”
增强for循环写起来很爽,但它不一定总是性能最优解。比如遍历ArrayList,用传统的for (int i = 0; i < size; i++)配合get(i),因为底层的随机访问很快,有时会比迭代器略快一丢丢,因为省去了迭代器对象的创建和hasNext调用的开销。
但是遍历LinkedList时,传统下标访问就是灾难:每次get(i)都要从头走到第i个节点,时间复杂度O(n²)。而用迭代器从头到尾逐个走,时间复杂度O(n)。所以选哪种方式,要看你面对的数据结构是什么。
我实际开发中的习惯是:除非在性能敏感的循环里,否则一律用增强for或迭代器。 因为代码可读性和维护性带来的收益,通常远大于那一点点微小的性能损耗。等真正遇到性能瓶颈,再用性能分析工具定位也用不迟。
5.4 忘记实现Iterable导致增强for用不了
Java里经常出现这种情况:自己定义了一个集合类,想直接在for循环里遍历,结果编译报错。原因就是没有实现Iterable接口。动手写这个接口其实很简单,关键就两部分:
java复制public class MyCollection<T> implements Iterable<T> {
// 集合内数据...
@Override
public Iterator<T> iterator() {
return new MyIterator();
}
}
然后里面写一个实现了Iterator<T>的内部类。有时候你会看到有人直接返回一个匿名内部类:
java复制@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < items.size();
}
@Override
public T next() {
return items.get(index++);
}
};
}
这种写法代码集中,结构清晰,适合迭代逻辑比较简单的场景。
5.5 主从调度场景里的“无穷迭代”问题
回到前面说的智能体主从调度。如果你在主Agent里用迭代器遍历任务队列,一定要处理好“动态添加任务”的情况。因为主循环在迭代过程中,如果子Agent执行时又往任务队列里塞了新任务,你的迭代器就必须决定:是直接跳过还是继续处理。
这个时候,Java的ConcurrentModificationException就会教做人。解决办法是放弃“严格迭代”,改用并发队列(如ConcurrentLinkedQueue)配合poll()循环,把迭代器模式换成“生产者-消费者”模式。这也是设计模式应用里很重要的一点:模式不是唯一解,场景适配才是关键。
6. 面试、期末和大作业里怎么讲迭代器模式
最后说点应试和写作业的实用心得,毕竟热词里“设计模式期末”“设计模式大作业”占了不少。
如果面试问到迭代器模式,我建议按这个顺序回答:先用一句话说清楚“它不暴露集合内部结构,把遍历逻辑封装成独立迭代器”;然后说出四个角色;再举一个for (String s : list)底层就是迭代器的例子;最后一定要提到“遍历时删除元素用迭代器的remove方法”这个坑点。这样既展示了理解深度,又体现了实践经验。
写大作业时你可以在结构上多花心思。比如做一个文件目录遍历工具:目录用组合模式组织,遍历用迭代器模式封装,提供前序遍历和按扩展名过滤的迭代器,最后前端展示时统一走迭代器接口。这样整个作业就把组合模式和迭代器模式串联了起来,能明显拉开和普通作业的差距。
期末复习时,有个特别管用的自测方法:把模式的名字遮住,只给你一个场景,你能不能自己说出该用哪个模式。 比如:“我需要在不暴露内部结构的情况下,让外部代码统一的顺序访问我的某个对象集合”,你说得出迭代器模式,那就说明真学进去了。反过来只背名字背定义,一换个场景就傻眼,那基本等于白学。
我在实际项目里最大的感受是,迭代器模式是那种“用的时候不觉得神奇,一旦离开才知道有多重要”的隐形功臣。Java和C++两大生态把它做成了语言级的功能,Java的增强for、Stream,C++的STL算法库,全都建立在这个看似简单的“有下一个就取出来”之上。理解了它的原理,你不仅能写出更稳定、更优雅的遍历代码,以后看到那些“高深”的新框架、新范式,你也能一眼认出它们骨子里用过的这些经典设计。这大概就是学设计模式最大的回报。
