1. 从一个极其普遍的编码痛点说起
我做了十几年开发,迭代器模式是我见过最“隐形”、却又最无处不在的设计模式——说它隐形,是因为大多数程序员天天在用,却几乎意识不到自己正踩在迭代器模式的肩膀上。
就拿最基础的需求来说:你有一个列表,里面存了一批订单数据,你需要把它们全部取出来,逐个处理。最原始、最直觉的写法是什么?一个 for 循环加上 get(i)。
java复制for (int i = 0; i < orderList.size(); i++) {
Order order = orderList.get(i);
// 处理订单
}
这段代码有问题吗?在小项目里,没有任何问题。但当你的集合从 ArrayList 换成了 LinkedList,或者换成了 HashSet,甚至换成了存放于数据库游标中的记录集时,问题就来了。不同集合的数据组织方式和访问方式完全不一样,数组有下标,链表没有随机访问能力但可以通过节点指针逐个后移,哈希表干脆是一堆桶结构,数据库游标则是一次性顺序消费……如果你在每个业务代码里都写针对具体集合的遍历细节,那么一旦底层集合替换,所有上层遍历代码全要跟着改。
迭代器模式解决的就是这个核心痛点:把“怎么遍历”和“遍历什么”解耦,让遍历行为对上层使用者完全屏蔽内部结构差异。用一个统一的“游标”概念,把访问逻辑封装进去,集合外部的人只认 hasNext() / next() 这两个动作,至于内部是链表还是数组,根本不关心。
这就是设计模式的魅力——它从来不是在教你怎么写一个新功能,而是在教你怎么把那些反复出现的、普遍存在的结构性问题用成熟的范式去解决。迭代器模式作为 GoF 23 种设计模式中行为型模式的代表之一,它的价值不在于代码本身有多神奇,而在于它定义了“一种统一访问集合内部元素,且不暴露内部表示”的标准做法,而这一标准做法后来几乎被所有现代语言内置吸收。
不管你是 Java、C++、Python、JavaScript 开发者,也不管你是刚学设计模式的新手,还是需要在项目中做架构决策的资深开发,搞清楚迭代器模式的原理和边界,都值回票价。这篇就把我认为最核心的内容拆开了讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迭代器模式在解决什么问题:一次集合替换引发的连锁修改
迭代器模式的官方定义是:提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。定义很精简,但真正理解它还要从它试图解决的问题出发。
2.1 紧耦合的遍历代码是怎么把你拖垮的
假设你维护一个老项目,项目中有一个 OrderManager 类,里面用一个数组保存订单,所有的业务代码都依赖这个数组的遍历方式:
java复制public class OrderManager {
private Order[] orders = new Order[100];
private int count = 0;
// add、delete 等省略
}
半个月后业务量上去了,数组的容量不够用,定长结构导致扩容逻辑写起来痛苦不堪。你决定换成 ArrayList<Order> 或 LinkedList<Order>,性能上各有取舍,但不管换成哪个,所有写过 for (int i = 0; i < manager.getCount(); i++) { Order order = manager.orders[i]; } 的调用方代码,全部要跟着改一遍。
这是最典型的「面向具体实现编程」的代价。乍看之下每次改动都不算大,但随着系统膨胀,这些散落各处的遍历代码会变成一个个隐形的定时炸弹——你永远不知道底层存储换掉之后,还有哪段野代码在用旧的下标去访问。
2.2 迭代器模式给出了怎样的解耦思路
迭代器模式的思路其实异常朴素:既然每种集合都可能有一种最合适的遍历方式,那我就把这些遍历方式各自封装成独立的类,再让它们实现同一个访问接口。外部调用方只依赖这个接口,完全不用关心自己拿到的是数组、链表、哈希表还是自定义数据结构。
打个比方,这有点像你去餐厅点一份烤鸭。不管这家店的烤鸭是果木烤的还是电炉烤的,不管片鸭师傅的刀法是横切还是斜切,你作为顾客只需要知道“端上来的是片好的烤鸭,可以直接卷饼吃”。如果你非要跑进后厨,盯着鸭胚、盯着烤炉、盯着师傅切的每一刀,那只要店里换一种烤法,你的整个“吃饭流程”就崩溃了。迭代器就是那个“端菜的服务员”,它把后厨的复杂度全部隔离掉,给你一个清晰的“吃鸭”界面。
这个朴素的解耦思路价值极大:上层代码不再感知集合内部结构,底层集合替换时,只需配套提供一个新迭代器,所有上层逻辑零改动。用一个形象的代码示意:
java复制Iterator<Order> it = orderManager.createIterator();
while (it.hasNext()) {
Order order = it.next();
// 无论 orderManager 内部是数组还是链表,这层代码完全不变
}
这层稳定、统一、可替换的抽象,就是迭代器模式的核心贡献。聊清楚了它要解决的问题,再来看它的各个成员是如何分工配合的,就顺理成章了。
3. 迭代器模式的核心角色拆解:四个成员,各司其职
GoF 给迭代器模式定义了四个主要角色,理解这四个角色就等于拿到了整个模式的骨架。
- Iterator(迭代器接口):定义访问与遍历元素的统一方法。最经典的是
first()/next()/hasNext()/currentItem(),在现代语言中一般收敛为hasNext()和next()两个核心方法。 - ConcreteIterator(具体迭代器):实现迭代器接口,维护一个“当前位置”的游标,记录遍历进度。这是整个迭代器模式的“大脑”,所有遍历算法细节都封装在这里。
- Aggregate(聚合对象接口):定义创建迭代器的方法,例如
createIterator()。它约定“凡是可以被迭代的集合,都能创建一个对应的迭代器”。 - ConcreteAggregate(具体聚合对象):实现聚合对象接口,内部持有真实的数据结构,同时负责返回一个适配该结构的 ConcreteIterator 实例。
3.1 为什么要把“游标状态”单独抽成一个对象
很多初学者最大的疑问是:为什么不直接在集合类内部维护一个游标,非要多此一举搞一个迭代器对象?
这是迭代器模式最精妙的地方。如果游标是集合内部的字段,那同一时刻多个地方同时遍历这个集合时,游标字段就会被互相覆盖——A 遍历到一半,B 开始遍历,直接把 A 的当前位置冲掉了,A 下一次 next() 就不知道跳到哪去了。把游标独立成迭代器对象,本质上是把“遍历状态”从一个对象中抽离成独立对象,允许多个遍历互不干扰。
我还经常用“浏览器开多个标签页”来类比:集合好比一个网站,内部游标如果存在服务器端,那每个用户共享一个浏览位置,彼此打架;迭代器好比每个用户浏览器里的独立标签页,位置状态各自保存,互相不知道对方看到哪了。
3.2 自己画一张最简单的迭代器架构草图
如果你尝试画一下类关系,会发现这个模式的耦合度极低。聚合接口依赖迭代器接口,具体聚合实现聚合接口并创建具体迭代器,具体迭代器实现迭代器接口并持有一个具体聚合的引用。整个过程中唯一需要被外部见到的就是两个接口,所有实现细节全部封闭在背后。
这种低耦合的架构带来的直接好处有两个:一是新增一种遍历方式不需要修改集合源码,只需要新增一个迭代器实现类,符合开闭原则;二是聚合对象可以同时提供多种不同遍历顺序的迭代器——比如正序遍历、倒序遍历、按条件过滤遍历,只需要提供多个迭代器类即可,集合本身根本不用关心这些策略。
角色理清了,下面直接撸代码,你才能感受到这四个角色是如何落地的。
4. 手写一个迭代器:从零实现能用的可迭代容器
理论说得再多,不如自己动手写一遍。这里我用 Java 写一个最精简但五脏俱全的例子:自定义一个可以存放任意对象的简单列表,并给它配上一个完整的迭代器。这样你能清晰看到四个角色的代码长什么样子。
4.1 定义迭代器接口(Iterator)
尽管 Java 已经提供了 java.util.Iterator,但为了演示迭代器模式的原始结构,我先自定义一个最简单的接口,里面只放最核心的两个方法:
java复制public interface MyIterator<T> {
boolean hasNext();
T next();
}
hasNext() 负责回答“还有没有下一个”,next() 负责取出当前元素并把游标推到下一位。这两个方法共同构成了顺序遍历的最小充分条件。有些教材里还会加上 remove()、reset() 等方法,但那些都是基于具体场景的功能扩展,并不影响模式本身。
4.2 定义聚合接口(Aggregate)
聚合接口需要一个创建迭代器的方法,在 Java 生态中最熟悉的对应物就是 Iterable 接口里的 iterator() 方法。自定义的等价物长这样:
java复制public interface MyAggregate<T> {
MyIterator<T> createIterator();
}
任何实现了这个接口的集合,都在对外承诺:我可以给你一个游标,通过这个游标你能安全地访问到我的所有元素。
4.3 具体聚合类:自定义数组列表
为集中展示迭代器处理的复杂内部结构,我实现一个简单的动态数组容器,内部用 Object[] 存储数据,当容量不足时自动扩容。完整的代码包括增删查基础方法,但遍历相关的逻辑完全不暴露给外部。
java复制public class MyArrayList<T> implements MyAggregate<T> {
private Object[] elements;
private int size = 0;
public MyArrayList(int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("初始容量不能为负数");
}
this.elements = new Object[initialCapacity];
}
public MyArrayList() {
this(10);
}
public void add(T element) {
ensureCapacity(size + 1);
elements[size++] = element;
}
@SuppressWarnings("unchecked")
public T get(int index) {
checkIndex(index);
return (T) elements[index];
}
public int size() {
return size;
}
private void ensureCapacity(int minCapacity) {
if (minCapacity > elements.length) {
int newCapacity = elements.length + (elements.length >> 1);
elements = java.util.Arrays.copyOf(elements, newCapacity);
}
}
private void checkIndex(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("索引越界: " + index + ", 大小: " + size);
}
}
@Override
public MyIterator<T> createIterator() {
return new MyArrayListIterator<>(this);
}
}
4.4 具体迭代器类:真正干活的游标
重点来了。MyArrayListIterator 持有聚合对象的引用,同时维护一个索引作为当前位置,这是迭代器模式的核心状态。当初 ArrayList 内部迭代器也是这么干的,只是细节更丰富。
java复制public class MyArrayListIterator<T> implements MyIterator<T> {
private final MyArrayList<T> list;
private int cursor = 0;
public MyArrayListIterator(MyArrayList<T> list) {
this.list = list;
}
@Override
public boolean hasNext() {
return cursor < list.size();
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException("已没有下一个元素");
}
return list.get(cursor++);
}
}
4.5 测试一下这个迭代器是否好用
分别用直接索引访问和迭代器访问两种方式遍历同一个列表,然后测试多个迭代器并发遍历,验证互不干扰:
java复制public class Main {
public static void main(String[] args) {
MyArrayList<String> list = new MyArrayList<>();
list.add("设计模式");
list.add("迭代器模式");
list.add("行为型模式");
System.out.println("== 使用迭代器遍历 ==");
MyIterator<String> it = list.createIterator();
while (it.hasNext()) {
String item = it.next();
System.out.println(item);
}
System.out.println("== 两个迭代器并发遍历(互不干扰) ==");
MyIterator<String> it1 = list.createIterator();
MyIterator<String> it2 = list.createIterator();
System.out.println("it1: " + it1.next()); // 设计模式
System.out.println("it1: " + it1.next()); // 迭代器模式
System.out.println("it2: " + it2.next()); // 设计模式 —— it2 从头开始
System.out.println("it1: " + it1.next()); // 行为型模式
}
}
运行结果完全符合预期:it1 推进到第二个元素时,it2 依然可以从头遍历。这个例子直观证明了“遍历状态独立保存”的核心设计收益。实际项目中这就是最常见的遍历场景之一——处理订单列表时,一个线程按入库顺序遍历计算总额,另一个线程同时按紧急程度过滤需要人工介入的订单,两者互不干扰。
4.6 分析一下代码中的关键设计与取舍
看明白代码后,我用三点总结这段实现的设计逻辑:
- 遍历接口最小化:接口里只有
hasNext()和next(),没有塞入多余方法。调用方只需要理解这两个方法即可完成完整的顺序遍历。这是接口隔离原则的直接体现。 - 游标状态内聚:索引
cursor被封装在迭代器内部,除了迭代器自己没有任何代码能修改它。这保证了遍历过程的确定性——只要迭代器内部逻辑没 bug,游标就不会无端跳位。 - 集合类变得干净:
MyArrayList只关注数据存储和扩容等基本功,遍历算法全部分派给迭代器。单一职责原则让两个类各自可以独立演化,集合内部结构调整时不影响迭代器接口层面。
我建议每个学习设计模式的人都亲手写一遍这段小代码。写完后你会对“遍历”这个词有一个结构性的理解,而不是停留在使用语言自带 for-each 的舒适区。
5. 深入源码看迭代器:从 JDK 的 ArrayList 说起
如果说手写代码让你理解了迭代器的理论骨架,那阅读 JDK 源码能让你看到这套理论在工业级代码里的完整进化形态。我从 JDK 里最常用到的集合说起,把迭代器在其中的应用解剖一遍。
5.1 ArrayList 的内部迭代器 Itr 是怎么设计的
JDK 中 ArrayList 有一个内部私有类 Itr,它实现了 Iterator<E> 接口。核心字段有三个:
cursor:下一个要返回元素的下标,等价于我的手写示例里的游标。lastRet:最近一次返回元素的下标,初始为 -1。这个字段专为支持remove()方法设计。expectedModCount:预期的结构修改次数,初始值为外部ArrayList的modCount,专为 fail-fast 设计。
hasNext() 的实现简洁到一眼看懂:
java复制public boolean hasNext() {
return cursor != size;
}
因为 ArrayList 的底层是连续数组,下标从 0 到 size-1,一旦游标等于 size,说明遍历到尾巴了。
next() 里有一段最值得品味的处理:
java复制public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
注意一个细节:JDK 在 next() 中先拿 elementData 的局部引用,再检查 i >= elementData.length。为什么要做这个看似多此一举的检查?因为多线程环境下,可能存在另一线程在你调 next() 期间对 ArrayList 做了结构性修改——比如清空、扩容后其内部数组被替换。此时 cursor 指向的下标可能已经超出新数组长度,如果不检查,elementData[i] 会直接抛出 ArrayIndexOutOfBoundsException。JDK 强行为这个底层异常换成了更语义明确的 ConcurrentModificationException,方便上层排查并发修改问题。
从手写代码到 JDK 源码,你会发现迭代器模式的骨架没变,但工业级实现把所有边界条件、并发安全、异常语义都考虑进去了。这正是设计模式“实践出真知”最好的注脚。
5.2 modCount 与 fail-fast:迭代器背后的安全机制
modCount 是全套 fail-fast 机制的基石。ArrayList 的每次结构性修改(add、remove、clear 等会改变元素数量的操作)都会让 modCount 加 1,而迭代器在创建时记录下当时的 expectedModCount。之后每次调用 next() 或 remove() 时,迭代器会比对自己的 expectedModCount 与外部 ArrayList 当前的 modCount,不一致就抛出 ConcurrentModificationException。
java复制final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这套机制看起来简单,却异常重要。想一想迭代器的本质:它维护了一个游标位置,同时假设底层集合的结构在遍历过程中不会剧变。如果遍历期间有人插入或删除了元素,元素的位置整体移动,游标后面的遍历结果就不可信了。与其让你在业务深处拿到诡异的脏数据,不如尽早、明确地把错误抛出来。fail-fast 的本质是:宁可快速失败,绝不带病运行。
一个典型场景是边遍历边删除:
java复制List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if ("b".equals(s)) {
list.remove(s); // 这行会抛 ConcurrentModificationException
}
}
这段代码跑起来后,当 next() 返回 "b" 后再次进入循环时,checkForComodification() 发现 modCount 已被 remove 改过,立刻抛出异常。正确的做法是使用迭代器自己的 remove() 方法:
java复制Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("b".equals(s)) {
it.remove();
}
}
因为 Itr.remove() 会同步把 expectedModCount 更新为最新的 modCount,迭代器自身的修改不会引发校验失败。
5.3 ListIterator 的扩展思路
除了标准 Iterator,ArrayList 还提供了 ListIterator,它在 Iterator 基础上扩展了反向遍历和双向修改能力:
hasPrevious()/previous():往前遍历。nextIndex()/previousIndex():获取当前游标两侧的索引。set(E e):替换最近一次返回的元素。add(E e):在当前游标位置插入元素。
ListIterator 的价值在于它印证了迭代器模式的开放扩展能力。迭代器接口本身是一个高度抽象的“游标”概念,当容器需要支持更丰富的遍历语义时,只需扩展迭代器接口,而不需要改动聚合对象内部的数据存储代码。这也是为什么在很多框架里,Iterator 和 ListIterator 可以按需选择使用。
5.4 HashMap 的迭代器为什么是“弱一致”的
与 ArrayList 的强一致性 fail-fast 检查不同,HashMap 的迭代器在设计上有微妙的差别。HashMap 内部有链表和红黑树结构,它的 HashIterator 类在遍历原理上虽然也检查 modCount,但由于哈希表的扩容和树化过程,迭代器在实际遍历时访问的桶位可能与当前 HashMap 结构并非完全一致,业界称这种设计为“弱一致”。
这种差异导致实际使用中你很可能遇到一种现象:遍历 HashMap 时,某一刻你删除了某个 key,然后继续遍历,居然没有抛异常,或者遍历结果里出现了一些模棱两可的元素。这在业务上是个隐藏的坑。凡是涉及多线程场景、边遍历边修改的场景,不管什么集合,最好的策略都是:要么用迭代器自身的修改方法,要么完全不修改只做读取,要么用并发容器如 ConcurrentHashMap。
阅读源码最直接的价值,是让你知道集合框架里这些“看似神奇”的高级功能,本质上都是迭代器模式加各种防御性编程组成的组合拳。理解之后再踩坑时,你能飞快地定位到是哪一层机制抛出的异常,而不是原地蒙圈。
6. 各语言对迭代器模式的吸收与演化
迭代器模式是少有的“全民普及度”极高的设计模式,几乎所有主流语言都把它的思想内建成了语法或标准库。对比各种语言的实现,能让你更清楚什么思想是模式里永恒不变的,什么只是特定语言的语法糖。
6.1 Java:接口驱动的迭代器
Java 的做法最贴合 GoF 原始定义——定义 Iterator 和 Iterable 两个接口,任何实现 Iterable 的集合都能用 for-each 语法遍历:
java复制for (String s : list) {
System.out.println(s);
}
这个 for-each 语法糖编译后,底层就是创建迭代器并循环调用 hasNext() / next()。Java 把“接口统一”和“语法糖”结合得很好,让绝大部分开发者无感知地使用着迭代器模式。
但 Java 的迭代器有一个明显局限:它是外部迭代器,即控制权在使用者手中——由你显式调 hasNext() / next(),或者在 for-each 中隐式做这些动作。这意味着整个遍历流程的“骨架”是被业务代码定义的。
6.2 C++:一个 STL 头文件里的精妙设计
C++ 的 STL(Standard Template Library)把迭代器推向了另一个极端。STL 中的迭代器不是一个固定接口,而是一组概念(concept),被分为输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器五种级别。每种级别承诺一组操作:
- 输入迭代器:支持
*it和++it。 - 随机访问迭代器:额外支持
it + n、it - n、it[n]、比较大小。
标准算法 std::sort、std::find、std::lower_bound 等全部围绕迭代器实现,容器的底层数据结构被彻底隐藏:
cpp复制std::vector<int> vec = {3, 1, 4, 1, 5};
std::sort(vec.begin(), vec.end());
// 输出:
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
如果你换成本质上不同的 std::list,同一段 std::sort 会因为链表不具备随机访问迭代器而编译不通过。这种编译期约束确保了用法合法性在编译阶段就被发现,而不是留到运行时抛异常。C++ 的迭代器设计核心是“零开销抽象”,所有遍历细节都在编译期被解析,运行时不产生任何多余分支或虚函数调用。
6.3 Python:协议驱动的 for-in 魔法
Python 选择了与 Python 语言自身气质最匹配的实现方式——协议。只要一个类实现了 __iter__() 和 __next__() 方法,它就能被 for 循环使用:
python复制class CountDown:
def __init__(self, n):
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n
for num in CountDown(5):
print(num) # 4 3 2 1 0
在内部,Python 的 for 循环会先调用 iter(obj) 拿到迭代器,然后反复调用 next(it),直到 StopIteration 异常被抛出时循环结束。Python 的方法可以看作“异常驱动遍历终止”的典型代表——next() 不返回结束标志,直接用异常告诉你“没货了”。初学 Python 的人会觉得 StopIteration 诡异,但熟悉后你会发现这种设计给“暂停/恢复”类生成器留下了完美接口。
6.4 三种语言的共同点与差异
用表格对比一下三种语言的设计思路:
| 语言 | 驱动方式 | 终止信号 | 典型场景 |
|---|---|---|---|
| Java | 接口显式驱动 | hasNext() 返回 false |
集合框架、for-each 语法糖 |
| C++ | 操作符重载与模板 | 迭代器与 end() 比较 |
STL 算法、泛型编程 |
| Python | 协议方法驱动 | StopIteration 异常 |
生成器、for-in 语法 |
如果深入去抠它们的内核,会发现所有语言都遵守了同一个黄金法则——上层遍历逻辑不感知容器内部结构。C++ 的 vector 和 list 提供的迭代器操作外观一致(尽管能力级别不同);Python 的列表和自定义类的 for-in 写法完全一致;Java 的数组和 ArrayList 在 for-each 眼里几乎无差别。这就是迭代器模式二十多年经久不衰的根本原因。
7. 实战决策:什么时候该自己写一个迭代器
说句实在话,除了学习设计模式和面试需求,大多数现代项目中,你很少需要自己写迭代器。标准库已经覆盖了绝大部分常见需求。但有一些场景,自己实现迭代器会让代码的干净程度发生质变。
7.1 自定义数据结构该不该提供迭代器
如果你在项目里设计了一个自定义的容器类,比如树结构、图结构、带特殊业务规则的列表,我强烈建议让它实现 Iterable。不然,所有需要遍历这个容器的调用方都要先知道你的容器内部暴露的是什么方法,一旦内部结构变化,调用方全部遭殃。
举一个实战例子:订单系统里有一个 OrderTree,内部用树结构维护订单之间的父子关系。如果它实现 Iterable<Order> 并以中序遍历返回订单,那么前端展示、报表统计、批量操作等就都能用统一的 for-each 完成,不需要各自写递归:
java复制public class OrderTree implements Iterable<Order> {
private TreeNode root;
@Override
public Iterator<Order> iterator() {
return new InOrderTreeIterator(root);
}
}
调用方使用起来:
java复制for (Order order : orderTree) {
double total += order.getTotalPrice();
}
这种改造成本极低,收益却长期有效——后续即使树的存储结构从朴素二叉树平衡成红黑树,只要迭代器适配好,业务代码一行都不用动。
7.2 数据库游标和流式数据处理也适用同样思想
真实项目中,数据往往不是全部躺在内存里的。分页查询数据库、从消息队列批量拉取消息、处理千万级日志文件时,如果先把数据全部加载到内存再遍历,内存瞬间爆掉。此时迭代器模式能帮你做到“逐条获取,按需处理”。
一种典型做法是封装一个按页加载的迭代器,业务层完全只面向迭代器接口编程:
java复制public class PagedOrderIterator implements Iterator<Order> {
private final OrderMapper orderMapper;
private final int pageSize;
private int currentPage = 0;
private Iterator<Order> currentPageOrders;
private boolean hasMore = true;
@Override
public boolean hasNext() {
return currentPageOrders != null && (currentPageOrders.hasNext() || hasMore);
}
@Override
public Order next() {
if (currentPageOrders == null || !currentPageOrders.hasNext()) {
loadNextPage();
}
return currentPageOrders.next();
}
private void loadNextPage() {
List<Order> page = orderMapper.selectPage(currentPage++, pageSize);
hasMore = page.size() == pageSize;
currentPageOrders = page.iterator();
}
}
这样业务层可以这样写:
java复制Iterator<Order> it = new PagedOrderIterator(orderMapper, 1000);
while (it.hasNext()) {
process(it.next());
}
看起来只是换了个遍历对象,但内里已经完成了“内存中永远只保留 1000 条记录、按需从数据库加载”的大型作业降级,业务代码完全没有感知。这是迭代器模式在真实系统里非常有价值的一类变体运用。
7.3 业务规则需要屏蔽复杂容器内部细节的时候
另一个高频场景是:你有一个“复合数据体”,它的内部结构很复杂——多个列表、多个对象、甚至多个外部数据源糅合在一起。外部业务只想知道“能不能继续取订单”,不想知道你内部到底混了多少个来源,此时就可以实现一个复合迭代器,内部负责从多个子源流转。
比如一个客户资产总览接口,需要把活期存款、定期存款、理财产品、基金持仓全部合并成一个统一的“资产流水”顺序遍历输出。你完全可以写一个组合迭代器,它内部把多个资产子迭代器串联起来。这种做法让上层逻辑只关心资产流水的顺序,不关心资产类别的内部差异。
7.4 一句话判断标准
什么时候“值得”手写迭代器?我给大家一个简单判断标准:
- 你有一个容器/数据源,需要被多个地方遍历,但每个地方都不想关心它内部长什么样;
- 你正在进行流式或分页类数据读取,希望统一遍历接口但不想把分页细节泄漏到业务代码;
- 你需要在不污染容器类本身的前提下扩展多种遍历规则。
如果只是对 ArrayList 或 HashMap 做普通遍历,请老老实实用标准库的迭代器或 for-each,千万别为了“炫技”而手写迭代器类——那是过度设计。
8. 使用迭代器时最容易翻车的五个细节
作为用过迭代器模式很多年的人,我把实战中大家最容易踩的坑集中列一下。这些细节在理论资料里很少系统性提及,但真实场景中几乎都遇到过。
8.1 多线程遍历下的数据一致性
前文说过的 modCount 机制在单线程中十分可靠,但多线程环境下 ArrayList 的迭代器并不能保证一致性。一个线程迭代读、另一线程迭代写时,读线程大概率抛出 ConcurrentModificationException,但如果写线程在next() 的检查间隙完成了修改,读也可能拿到不一致数据。解决思路是别让裸 ArrayList 在多线程之间共享可变数据,使用 CopyOnWriteArrayList 或 ConcurrentHashMap 来弱化冲突:
java复制List<String> list = new CopyOnWriteArrayList<>();
// 遍历与修改并发进行时,迭代器基于快照,不会抛 ConcurrentModificationException
8.2 迭代器是一次性“消耗品”
迭代器多半是有状态的游标,初始化创建后遍历过一次,到尾部就废弃了。想重新遍历必须重新创建迭代器。这个点看似基础,但常有人犯迷糊——在一个循环里把同一个迭代器传给多个方法,第二个方法拿到的迭代器已经从上次的位置继续了,数据少了一段。解决办法很清楚:每次遍历,都新建一个迭代器对象,别复用。
8.3 自定义迭代器时 next() 与 hasNext() 的时序
写自定义迭代器最常见的 bug 是游标推进时机出错。你在业务里见到的最稳定的迭代器语义是:hasNext() 只做“看”,不推进;next() 做“取”并推进。如果你把推进动作放到了 hasNext() 里,那别人写 if (it.hasNext()) { otherProcessing(); it.next(); } 时可就要出大问题——hasNext() 早把元素“吃”掉了。记住这个铁律:查询操作不能修改状态;修改状态的操作只能发生在明确的获取动作中。
8.4 空集合、单元素集合与中途删空的边界
写迭代器实现时,最容易漏掉三个边界场景:
- 元素为 0:
hasNext()必须返回 false,next()必须抛异常或返回空数据。 - 元素为 1:创建迭代器后调用一次
next()到达末尾,第二次next()要能优雅失败。 - 遍历中途把所有元素删除:迭代器发现一个都不剩,要能够正常终止而不是死循环。
建议自测用例至少覆盖这三类情况,再提交代码。
8.5 老代码“边 for-each 边改数据”的坏味道
在 for-each 或迭代器遍历循环体内直接调用集合的 add / remove / clear 方法,几乎都会触发 fail-fast 异常。最稳妥的替代方案是:先收集要删除的元素,遍历结束后再统一删,或使用迭代器的 remove(),或直接用 Java 8 的 removeIf(Predicate) 这种声明式写法。后者底层其实做了迭代器封装,语义既清楚又安全:
java复制list.removeIf(s -> s.startsWith("temp"));
这里面的经验归纳起来就是一句话——在你决定对集合进行修改之前,先想清楚你手上拿到的到底是一个集合对象还是一个带有游标位置的迭代器,二者混淆起来,灾难就在眼前。
9. 迭代器与组合模式、访问者模式搭配时的协作价值
设计模式很少孤立使用,迭代器模式也不例外。在许多框架代码里,它常常跟组合模式、访问者模式互相配合,形成一个更完整的解决方案。
9.1 迭代器 + 组合模式:让树形结构像扁平列表一样遍历
组合模式把对象组织成树形结构以表示“部分-整体”的层次关系,常见于菜单系统、文件目录、组织架构。问题随之而来——层次结构谁负责遍历?如果让业务层自己写递归,那递归逻辑散落各处,每一个业务场景都要重复一遍树遍历。这里迭代器就能解决:在组合结构上实现一个迭代器,它内部帮你完成深度的栈式遍历,把树压平成序列,返回给外部调用方。外部使用者看到的只是一串扁平的元素流。
很多菜单系统、权限树控件、文件目录树控件能“像遍历列表一样”遍历树节点,靠的就是这种组合迭代器。它让组合模式的表达力直接从“静态结构”提升到了“可灵活访问的动态序列”。
9.2 迭代器 + 访问者模式:遍历与操作解耦的极致
访问者模式解决的是“某一组对象结构稳定,但其上的操作经常变化”的问题。它需要遍历所有元素并对每个元素分派到对应的访问方法,而这个遍历过程恰恰可以由迭代器承担。迭代器负责“怎么走”,访问者负责“走到之后干什么”,两者分工明确,各自演化。
典型例子是编译器 AST 抽象语法树的处理。语法树结构相对稳定,但其上的分析操作(语法检查、类型推断、代码生成)经常增加。一个按顺序遍历语法树的迭代器,配合一组实现不同检查逻辑的访问者,可以极其灵活地在不改动 AST 结构的前提下,为它增加无数种处理逻辑。
9.3 这些协作关系给你的启发
很多人误以为设计模式是“一个萝卜一个坑”,每个模式都有严格的使用场景边界。其实从迭代器与其他模式的协作中你能看到,设计模式之间的边界是模糊的、搭档关系是普遍的。真正影响代码质量的,从来不是你会背多少个模式的名字,而是你能不能识别出“这里的核心变化点到底在哪里”,然后挑出最合适的模式或模式组合去封住变化。
从迭代器模式身上,我可以提炼出三个通用的设计思维,它们在我做架构决策时反复用到:
- 隔离变化:把“怎么遍历”从“存什么、怎么存”中抽出来,这是应对变化的基础。
- 依赖抽象:上层只依赖最小的接口(
hasNext()/next()),实现细节全部藏在底层。 - 最小充分承诺:接口只给调用方真正需要的承诺,不多不少,避免接口膨胀引发的连锁改动。
10. 写在最后:你真正该记住的迭代器的心法
迭代器模式是我平时跟团队里的小伙伴交流时,最不吝啬推荐大家去“手写一遍”的行为型模式之一。原因无他,它足够简单,简单到几乎每个人都能在半小时内写一个能跑通的例子;它又足够有代表性,把“封装变化”“面向接口”“单一职责”这些设计思想浓缩在一个极小的场景里。
作为一个常年跟集合、流、数据结构打交道的开发,我个人在实际项目中反复验证下来的体会是:迭代器模式的本质不是那个 Iterator 接口,也不是 hasNext() / next() 这两个方法,而是“你到底希望上层代码依赖什么”的决策能力。如果你希望上层依赖底层数据结构,那迭代器是多余的;如果你希望上层只依赖一套稳定的遍历语义,那迭代器就是不可替代的护栏。
最后给大家留一个小扩展方向:结合 Java 的 Stream API 再回头看看迭代器模式,你会发现 Stream 某种意义上已经把迭代器从“被动遍历”升级成了“主动计算管道”的模式——数据在流水线上流动、过滤、映射、聚合。它把大量集合内外的遍历、变换逻辑从命令式的循环中解放出来,变得声明式、可组合、易并发。此时,再回过头看迭代器当年的那套解耦理念,你会产生一种“似曾相识但格局变大”的感觉。
模式是死的方法,思维是活的开关。把迭代器摸透,你就顺带掌握了一根推开行为型设计模式大门的钥匙。
