做过业务系统的同学应该都有过这种经历:同一个列表数据,今天要按顺序遍历,明天要倒着遍历,后天又要过滤掉某些元素再遍历。如果每次都在业务代码里写一套for循环,改来改去不仅心累,还很容易把集合的内部结构暴露得干干净净。这时候你就需要认识一下迭代器模式——这个看似不起眼、却天天都在用的设计模式。
迭代器模式是我们日常开发中使用频率极高的一类行为型设计模式,核心就一句话:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。在Java的集合框架里,它被发扬光大;在C++的STL里,它更是贯穿始终的设计灵魂。这篇文章我不打算只念一遍教科书定义,而是结合真实项目场景,从原理到代码、从JDK源码到常见坑位,把迭代器模式彻底讲透。适合正在学设计模式的学生、准备面试的开发者,以及想在代码里少写几行重复遍历逻辑的工程人。
1. 迭代器模式的核心思想与设计本质
1.1 为什么需要迭代器:从一次"血泪遍历"说起
先说我之前接手过的一个老项目。系统里有一个订单列表模块,订单数据存在一个自定义的OrderCollection类里,内部用一个ArrayList存储。第一版需求很简单,就是按时间顺序展示订单,所以业务代码里直接写了:
java复制for (int i = 0; i < orderList.size(); i++) {
Order order = orderList.get(i);
// 处理订单
}
过了一阵子,产品说要改成按金额倒序展示。好,我在业务代码里换了个遍历方向。又过了一阵子,数据结构从ArrayList换成了LinkedList,因为订单量上来了,中间插入和删除变多了。结果get(i)的性能崩了,LinkedList的随机访问是O(n)复杂度,整个遍历变成了O(n²)。于是所有调用方都得跟着改。
这个场景你们是不是特别眼熟?问题就出在遍历逻辑和数据结构耦合在一起。调用方拿到一个具体集合类,就得知道它是数组还是链表,是有序还是无序,然后针对性地写遍历代码。一旦底层结构变化,所有遍历的地方全要重写。
迭代器模式就是冲着这个问题来的。它把"怎么遍历"这件事从集合类里抽离出来,定义成一个独立的迭代器对象。调用方只需要跟迭代器打交道,根本不需要关心背后是数组、链表、红黑树还是别的什么结构。
这里有个特别直观的类比:你家的电视遥控器。你只需要按"下一个频道"按钮,就能换台,根本不需要知道电视内部是显像管还是液晶面板,也不需要知道信号是怎么解码的。迭代器就是这个遥控器,"下一个"按钮就是next()方法。
1.2 四个角色:迭代器模式的标准配置
迭代器模式的结构非常清晰,就四个角色,我结合代码来分析:
| 角色 | 职责 | Java示例 |
|---|---|---|
| Iterator(迭代器接口) | 定义遍历所需的操作方法 | java.util.Iterator |
| ConcreteIterator(具体迭代器) | 实现具体遍历逻辑,记录当前遍历位置 | ArrayList里的Itr内部类 |
| Aggregate(聚合接口) | 定义创建迭代器的方法 | java.util.Collection |
| ConcreteAggregate(具体聚合类) | 返回具体的迭代器实例 | ArrayList、HashSet等 |
java复制// 迭代器接口
public interface Iterator<E> {
boolean hasNext(); // 是否还有下一个
E next(); // 返回下一个元素
default void remove() { throw new UnsupportedOperationException(); }
}
// 聚合接口
public interface Aggregate<E> {
Iterator<E> createIterator();
}
对应到具体的类,ArrayList就是ConcreteAggregate,它内部有一个私有内部类Itr implements Iterator,也就是ConcreteIterator。ArrayList.iterator()方法负责把Itr实例返回给调用方。
这里有个很多新手没注意到的设计细节:具体迭代器一般是聚合类的内部类。为什么?因为内部类可以无障碍访问外部类的私有字段。
Itr能直接操作ArrayList内部的elementData数组和modCount字段,天然拿到了遍历所需的全部信息,还不必暴露任何内部结构。
1.3 核心收益:解耦才是王道
从设计模式的角度讲,迭代器模式最大的价值在于解耦。具体表现在两个层面:
第一,遍历算法与数据结构解耦。同一个集合,可以有正序遍历、倒序遍历、跳跃遍历等多种迭代器,而不需要修改集合本身的代码。这符合开闭原则——对扩展开放,对修改关闭。
第二,客户端与集合内部结构解耦。客户端面向的是Iterator接口编程,而不是某个具体集合类。这样即使底层数据结构整个被替换,客户端的遍历代码一行都不用改。
我给你们画个业务场景对比,你们就明白这个解耦有多值钱了。假设有一个报表模块需要遍历多种数据源:数据库查询结果、Excel导入的数据、第三方接口返回的JSON数组。这些数据源内部结构完全不同,但如果它们都实现一个统一的createIterator()方法,报表模块的遍历逻辑就完全统一了。数据源的差异被迭代器挡在了外面,报表模块只看到一个接一个的对象。
2. 手写一个迭代器:从零实现彻底搞懂原理
2.1 自定义可迭代集合的完整代码
很多教材讲迭代器模式,都是从JDK源码开始,但我更推荐自己动手写一遍。只有自己写一遍,你才能真正体会到为什么迭代器要这么设计,以及哪些细节是踩过坑才学得会的。
我设计一个MyList类,内部用数组存储,提供迭代器功能:
java复制public class MyList<E> implements Iterable<E> {
private Object[] elements;
private int size;
private int modCount = 0; // 结构修改计数器
public MyList(int initialCapacity) {
this.elements = new Object[initialCapacity];
this.size = 0;
}
public boolean add(E e) {
ensureCapacity();
elements[size++] = e;
modCount++;
return true;
}
private void ensureCapacity() {
if (size >= elements.length) {
elements = Arrays.copyOf(elements, elements.length * 2);
}
}
@Override
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor = 0; // 下一个要返回的元素下标
int lastRet = -1; // 上一次返回的下标,用于remove
int expectedModCount = modCount; // 迭代器创建时的修改计数
@Override
public boolean hasNext() {
return cursor != size;
}
@Override
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
if (cursor >= size) {
throw new NoSuchElementException();
}
Object[] elementData = MyList.this.elements;
if (cursor >= elementData.length) {
throw new ConcurrentModificationException();
}
lastRet = cursor;
return (E) elementData[cursor++];
}
@Override
public void remove() {
if (lastRet < 0) {
throw new IllegalStateException();
}
checkForComodification();
MyList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
}
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
private void remove(int index) {
// 省略数组移位逻辑,正常时需要更新modCount
System.arraycopy(elements, index + 1, elements, index, size - index - 1);
elements[--size] = null;
modCount++;
}
}
这段代码是仿照JDK里ArrayList.Itr写的,但去掉了复杂边界处理,保留了核心逻辑。注意几个关键点:
cursor记录下一个要返回的元素位置,这个指针是迭代器自身的状态。多个迭代器同时遍历同一个集合时,各自的cursor互不影响。lastRet记录上次返回的元素下标,用于remove()操作。modCount是集合结构修改计数器,一旦集合在迭代过程中被结构性修改(增删元素),modCount变化,迭代器立刻检测到并抛出异常。
2.2 为什么用内部类实现具体迭代器
这里我要展开讲一个很多教程不会细说的设计决策:为什么具体迭代器一定要做成内部类。
最简单也最直接的答案:访问私有字段。MyList的elements数组和size字段都是private的,如果Itr是一个外部类,它就无法直接访问这些字段,只能通过MyList提供public的get(int index)和size()方法来间接访问。一旦走public方法,内部结构就暴露了一半,迭代器和聚合对象之间的紧密联系根本建立不起来。
第二个原因是归属关系清晰。Itr这个类存在的唯一意义就是为MyList服务,它跟MyList的生命周期是绑定的,没有必要暴露成一个独立的顶层类。用内部类,从代码结构上就明确了"这是我的一部分,不是给人随便new的"。
第三个原因涉及Java的内部类语法机制。内部类可以持有外部类实例的引用(即MyList.this),所以Itr里可以直接写MyList.this.elements来访问外部类的数组。这种语法糖让代码写起来特别自然,不需要额外传引用。
有人可能会问:那用匿名内部类行不行?行,JDK里某些集合就用了匿名内部类实现迭代器。但命名内部类的好处是可以用lastRet这样的状态字段保存迭代过程中的中间状态,匿名内部类虽然也能写字段,但可读性会差一些。我个人的习惯是:迭代逻辑简单的一两行用匿名内部类,逻辑复杂、有状态就用命名内部类。
2.3 继承Iterable接口的额外好处
注意我的MyList实现的是Iterable接口,而不是自定义的Aggregate接口。这是有意为之,因为Iterable是Java的增强for循环(for-each)的底层依赖。
java复制MyList<String> list = new MyList<>(10);
list.add("设计模式");
list.add("迭代器模式");
for (String s : list) {
System.out.println(s);
}
这段代码之所以能编译运行,就是因为MyList实现了Iterable接口。编译器会把增强for循环展开成这样的字节码逻辑:
java复制Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
所以,只要你实现了Iterable接口,你的自定义集合类就自动获得了增强for循环能力,这是最便宜的语法糖入口。很多初学者不知道这一点,自己写了一个集合类,想用for-each遍历却发现编不过,其实就是没实现Iterable。
3. 源码视角:JDK里的迭代器到底有多讲究
3.1 ArrayList.Itr的完整分析
既然前面手写了一遍,现在再看JDK源码就会有似曾相识的感觉。java.util.ArrayList里的Itr内部类实现如下(基于Java 17,略有删减):
java复制private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
你们看,和我手写的版本几乎一模一样,除了JDK在next()里多了一个边界检查:if (i >= elementData.length)。这个检查是为了防止一种极端情况——在迭代过程中,集合被结构性修改了,导致size变小了但cursor还指向原来的位置,此时cursor可能越界访问elementData数组。JDK在这里用了一个双重保险:先检查modCount,再检查数组越界。
很多人不知道一个细节:ArrayList.Itr是fail-fast的。什么叫fail-fast?就是说这个迭代器一旦检测到集合在迭代过程中被修改了(modCount变了),就立刻抛出ConcurrentModificationException,而不是硬着头皮继续遍历,或者返回错误的数据。这是一种"宁可错杀一千,不可放过一个"的防御策略。
3.2 不同集合的迭代器差异对比
ArrayList的迭代器是顺序遍历数组下标,但这不是唯一的迭代方式。JDK里每种集合的迭代器,都对"如何遍历"这个问题给出了不同的答案:
| 集合类型 | 迭代器实现方式 | 遍历顺序 | 是否有序 |
|---|---|---|---|
| ArrayList | 数组下标递增 | 插入顺序 | 是 |
| LinkedList | 链表节点next指针 | 插入顺序 | 是 |
| HashSet | 哈希桶+链表/红黑树 | 依赖hash值,不稳定 | 否 |
| LinkedHashSet | 哈希桶+双向链表 | 插入顺序 | 是 |
| TreeSet | 红黑树中序遍历 | 自然顺序/比较器顺序 | 是 |
| ArrayDeque | 循环数组下标 | 从头到尾 | 是 |
看到没有,同样是"拿到迭代器然后一个一个next",不同集合的底层遍历逻辑天差地别。但关键在于——这些差异对客户端是完全透明的。你写代码的时候:
java复制Iterator<String> it = collection.iterator();
while (it.hasNext()) {
String s = it.next();
}
不管collection是ArrayList还是TreeSet,这段代码都不用改。这就是迭代器模式最有价值的地方。
这里有一个隐藏很深的点。HashSet的遍历顺序并不稳定,两个相同元素的HashSet,在不同JDK版本下遍历顺序可能不同。如果业务代码依赖了HashSet的遍历顺序,那是典型的错误用法。而TreeSet的迭代器保证按比较器排序输出,如果你需要有序遍历,应该选择TreeSet或LinkedHashSet,而不是指望HashSet恰好给你什么顺序。
3.3 ListIterator:迭代器的增强版
在List体系里,JDK还提供了一个ListIterator接口,它继承自Iterator,额外增加了反向遍历和迭代过程中修改元素的能力:
java复制ListIterator<String> listIt = list.listIterator();
while (listIt.hasNext()) {
String s = listIt.next();
}
// 反向遍历
while (listIt.hasPrevious()) {
String s = listIt.previous();
}
// 迭代过程中替换元素
listIt.set("新值");
ListIterator支持add()和set()操作,可以在迭代过程中安全地修改集合内容,而不会触发ConcurrentModificationException(因为它会同步更新expectedModCount)。
这个类的设计更进一步说明了迭代器模式的灵活性——在基础迭代能力之上,每个具体的聚合类可以根据自身特点扩展迭代器的能力。List因为有明确的索引概念,所以可以支持反向遍历;Set没有索引概念,就无法提供ListIterator。这是合理的设计取舍。
4. 迭代器模式在真实框架中的应用:不止是集合遍历
4.1 数据库游标与JDBC ResultSet
很多人以为迭代器模式只存在于内存集合中,其实数据库驱动里的ResultSet就是迭代器模式的思想。ResultSet.next()方法就是在移动游标,返回boolean表示是否还有下一行,getXxx()方法则是获取当前行的字段值。这不就是hasNext()和next()的数据库版吗?
我做数据导出功能的时候,经常要用到这种"流式游标查询"。如果用普通查询把一百万条数据一次性加载到内存里,JVM直接OOM。但如果用游标方式,一条一条拉取,内存占用就很平稳。这是迭代器模式在性能优化场景下的典型应用——提供惰性获取能力,而不是一次性加载全部数据。
MyBatis里的Cursor接口就是这种思想的Java实现。SqlSession.selectCursor()返回一个Cursor<T>对象,它实现了Iterable接口,并且是惰性加载的。当你的查询结果集很大时,可以考虑:
java复制try (Cursor<User> cursor = session.selectCursor("selectAllUsers")) {
for (User user : cursor) {
// 逐条处理,内存友好
}
}
注意这里用了try-with-resources,因为Cursor实现了Closeable接口。用完之后必须关闭,释放数据库资源。
4.2 Spring框架里的迭代器模式应用
Spring的CompositeIterator是一个组合了多个迭代器的实现。它用在Spring的AnnotationConfigApplicationContext注册bean定义时,需要从一个包含多个BeanDefinitionRegistry的列表里统一遍历。它的实现思路是:内部维护一个迭代器列表,遍历时先走完第一个迭代器,再走第二个,以此类推。这本质上是一种组合模式+迭代器模式的联合应用。
还有一个典型的例子是Spring MVC的CompositeMethodArgumentResolver,它是策略模式的典型代表,但遍历策略解析器列表的过程也大量使用迭代器模式的思想。
我自己平时写代码时也会用类似思路。比如在一个接口的多个实现类里做链路调用,可以用迭代器模式把实现类列表包装起来,业务逻辑只需要拿到迭代器,然后依次调用。这样后续增加或删除实现类,只需要改配置,业务代码完全不动。
4.3 多Agent系统中的主从模式与迭代器的关系
最近AI Agent相关的项目很火,我在实践多Agent系统时发现一个有趣的现象:主从Agent模式中,主Agent调度子Agent的方式,本质上就是一种迭代器模式的变体。
主Agent维护一个子Agent列表,然后逐个"调用"子Agent,获取结果后决定下一步。这个"遍历子Agent列表并逐个执行"的逻辑,完全可以抽象成迭代器模式。有个更进一步的思路是把子Agent看作"另类的工具调用"——主Agent不是硬编码依次调用子Agent,而是通过一个统一接口遍历并执行。这样当子Agent列表变化时,主Agent的调度逻辑不需要修改。这个设计让我对迭代器模式的应用场景有了新的认识——它不仅能遍历数据,还能遍历"行为"和"服务"。
如果你在做Agent相关的开发,可以试试把子Agent的调度遍历抽象成迭代器模式:定义AgentIterator,按序或按条件遍历子Agent列表并执行任务。这种方式让调度逻辑非常清晰,也容易扩展。
5. 迭代器模式的实战陷阱与排查指南
5.1 ConcurrentModificationException的来龙去脉
要说迭代器模式相关的最著名异常,非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
}
}
为什么?增强for循环底层是迭代器,当next()被调用时,迭代器会执行checkForComodification(),比较expectedModCount和modCount。list.remove(s)执行后modCount+1,但迭代器里的expectedModCount没变,两者不一致,异常就抛出来了。
那正确的删除方式是什么?用迭代器自己的remove()方法:
java复制Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("b".equals(s)) {
it.remove();
}
}
为什么迭代器自己的remove()没问题?看源码就知道,Itr.remove()在移除元素后,会执行expectedModCount = modCount,把期望值同步成最新值,这样后续next()的检查就能通过。
注意:迭代器的
remove()是唯一安全的删除方式,且它删除的是lastRet指向的元素,也就是最近一次next()返回的元素。所以在调用remove()之前,必须先调用一次next(),否则会抛IllegalStateException。
如果你用的是Java 8及以上,更推荐用Collection.removeIf():
java复制list.removeIf(s -> "b".equals(s));
这个方法内部也是用迭代器实现的,但把删除条件封装成了函数式接口,代码更简洁。
5.2 自定义迭代器最容易踩的四个坑
写自己的迭代器时,有几个细节是平时文档里不会强调的,我把自己踩过的坑总结一下:
坑一:忘记实现hasNext()的幂等性。 hasNext()可能会被调用多次,不能因为第一次调用返回true就消耗了内部状态。你的hasNext()必须保证不改变迭代器状态,可以重复调用。这个约束在while(it.hasNext())循环里尤其重要,因为每次循环条件判断都会调一次。
坑二:next()越界不抛NoSuchElementException。 当迭代器已经没有更多元素时,next()必须抛出NoSuchElementException,而不是返回null或越界数组。这不仅仅是规范要求,更是为了避免客户端代码在边界条件下拿到错误数据。如果返回null,客户端可能误以为集合里存在一个null元素。
坑三:remove()被连续调用两次。 remove()通常只能紧跟在next()之后调用一次,第二次调用必须抛IllegalStateException。前面代码里用lastRet = -1来标记"上次next()的结果已经消费"就是这个目的。
坑四:迭代器对集合结构变化的容忍度。 完全不修改的集合不需要modCount,但如果你的集合支持增删操作,就必须考虑fail-fast机制。否则迭代过程中集合悄悄变化,客户端拿到的数据可能是错乱的,而且是静默错乱——这是最可怕的bug类型。
5.3 迭代器模式在哪些场景会引入设计复杂度
迭代器模式虽然好用,但不是银弹。我自己在项目里见过把迭代器模式用过头的情况,这里也提醒一下大家:
场景一:核心遍历逻辑极其简单,没必要抽象。 如果你只是对一个ArrayList做一次简单遍历,直接增强for循环就够了。强行引入自定义迭代器,只会增加代码量和阅读负担。设计模式的使用要恰到好处,不是为了用而用。
场景二:集合类本身已经是JDK集合类型。 如果是ArrayList、HashSet这些JDK自带的集合,直接用它们的迭代器就好,不需要再包装一层自定义迭代器。除非你确实需要做一些特殊的遍历控制,否则多层包装会降低性能、增加debug成本。
场景三:需要频繁随机访问元素。 迭代器模式适合顺序访问,如果你需要根据索引快速定位元素,应该直接暴露get(int index)方法,而不是用迭代器从头遍历到目标位置。
场景四:数据量极小且固定。 比如固定三个配置项要按顺序处理,直接写三行代码就行了,引入迭代器反而是过度设计。
6. 现代开发范式下的迭代器模式:Stream与协变思维
6.1 Stream API和迭代器模式的传承
Java 8引入Stream API后,很多人觉得迭代器模式是不是过时了。其实不是,Stream API的底层依赖仍然离不开迭代器思想。看这段:
java复制list.stream().filter(x -> x.getName() != null).map(User::getName).collect(Collectors.toList());
stream()方法会创建一个Spliterator(可分割迭代器),后续的filter、map操作都是在Spliterator的基础上做链式遍历。Spliterator是迭代器模式的现代演进版,它增加了一个很关键的能力:拆分。可以把一个大数据集拆分成多个子集并行处理,这就是Java并行流背后的秘密。
所以你可以认为:迭代器模式是遍历的1.0版本,Spliterator是遍历的2.0版本。思想一脉相承,只不过在并行计算时代,单纯的顺序遍历不够用了,需要支持分支合并的遍历器。
我个人在实际项目中,如果数据量不超过几万条,用Stream和用迭代器的性能差别可以忽略不计,优先选可读性更好的Stream。但如果数据量特别大,需要做分批处理或者流式读取,迭代器模式配合游标查询仍然是不可替代的方案。
6.2 迭代器模式的简单实现变体
除了标准的对象迭代器,现代开发中还有几种轻量级变体值得了解:
变体一:函数式接口+懒序列。 在Java里可以用自定义函数实现懒加载的迭代器,每次next()时才计算下一个值。这在处理无限序列或计算密集型序列时特别有用,比如斐波那契数列的无限流。
变体二:返回空迭代器。 不要返回一个null迭代器,而是返回Collections.emptyIterator()或Collections.emptyList().iterator()。这样客户端不需要做空指针判断,遍历逻辑更健壮。这在很多开源框架里是标准做法。
变体三:只读迭代器。 如果你不想让客户端通过迭代器的remove()方法修改集合,可以返回一个只支持hasNext()和next()、不支持remove()的迭代器,或者直接返回一个不可修改的集合视图(Collections.unmodifiableList())。这是保护数据完整性的重要手段。
7. 多个场景的完整案例:迭代器模式重塑报表引擎
我以前在一个报表平台项目中,用迭代器模式重构过一个数据源管理模块,这个案例正好能完整展示迭代器模式在真实项目里的落地方式,这里分享给大家。
7.1 需求背景:三种数据源,一套遍历逻辑
报表平台需要支持三种数据源:MySQL查询结果、Excel导入数据、第三方接口返回的JSON数组。最初的代码是每种数据源写一套遍历逻辑,报表模板引擎里堆满了if-else判断。后来加了一个新数据源(MongoDB),直接改吐了。
重构方案很简单:定义统一的ReportDataSource接口,每种数据源实现自己的迭代器,报表模板引擎只依赖Iterator<T>。
7.2 核心代码结构与实现要点
java复制public interface ReportDataSource<T> extends Iterable<T> {
String getName();
}
public class MySqlDataSource implements ReportDataSource<ReportRow> {
private Connection conn;
private String query;
@Override
public Iterator<ReportRow> iterator() {
return new MySqlResultIterator(conn, query);
}
}
public class MySqlResultIterator implements Iterator<ReportRow> {
private ResultSet rs;
private PreparedStatement pstmt;
public MySqlResultIterator(Connection conn, String query) {
try {
pstmt = conn.prepareStatement(query);
rs = pstmt.executeQuery();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean hasNext() {
// 这个设计有点坑:ResultSet第一次调用next()才能判断是否有数据
// 所以要引入一个fetched标志,避免重复调用next()
throw new UnsupportedOperationException("需要在具体场景中设计一次性游标逻辑");
}
}
这个案例里有一个非常经典的迭代器设计矛盾:JDBC的ResultSet是一次性游标,调用next()之后就无法回退。而标准的Iterator接口允许hasNext()被多次调用,且不能消耗迭代状态。这两者是冲突的,所以直接用ResultSet实现Iterator时,hasNext()的实现需要一个额外的状态缓存。
java复制public class MySqlResultIterator implements Iterator<ReportRow> {
private ResultSet rs;
private PreparedStatement pstmt;
private boolean hasFetched; // 是否已经调用过rs.next()
private boolean hasNextResult;
@Override
public boolean hasNext() {
if (!hasFetched) {
try {
hasNextResult = rs.next();
hasFetched = true;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return hasNextResult;
}
@Override
public ReportRow next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
hasFetched = false; // 重置状态,允许下一次hasNext()调用
try {
return mapRow(rs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
这个hasFetched标志就是关键。第一次调用hasNext()时,通过rs.next()尝试读取数据,把结果缓存起来。next()返回当前行后重置标志。这样hasNext()可以被安全地多次调用,不会破坏ResultSet的状态。
这种"缓存游标状态"的技巧,在实现各种基于外部游标的迭代器时都会用到。你自己写的时候一定要仔细设计状态机,否则很容易出现"遍历了一次就没了"或"跳行"的诡异问题。
7.3 重构后的效果
重构后,报表模板引擎的遍历逻辑变成了这样:
java复制public void render(ReportDataSource<?> dataSource) {
for (Object row : dataSource) {
// 统一的遍历逻辑
}
}
新增加MongoDB数据源时,只需要实现ReportDataSource接口并提供一个迭代器,报表引擎一行代码都不用改。这就是迭代器模式在真实项目中的核心收益,完全符合开闭原则。
8. 给不同语言使用者的迭代器模式实践建议
8.1 Java开发者应该掌握的迭代器细节
Java开发者在实际工程中,除了会用Iterator和Iterable,还应该掌握以下几点:
- 优先使用增强for循环和
forEach方法,不要手动写while(it.hasNext())循环,除非有特殊需求。 - 需要删除元素时,用
Collection.removeIf()或迭代器的remove(),不要在循环中调用集合自身的remove()。 - 遍历超大集合时,考虑使用流式游标(如MyBatis的
Cursor),避免内存溢出。 - 自定义集合时,实现
Iterable接口而不是直接暴露iterator()方法。
8.2 C++开发者看迭代器模式:STL的迭代器设计
C++的STL迭代器和Java的Iterator在概念上是相通的,但实现机制有很大差异。STL迭代器不是接口,而是行为像指针的对象,支持*it解引用、++it右移、it == end()比较等操作。它有五种类别:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器,每种迭代器支持的操作能力不同。
C++模板编程里大量使用迭代器作为算法和数据结构的中间桥梁。STL算法(如std::sort、std::find)接受的参数就是迭代器,而不是容器本身。这样同一个std::sort算法就能用于vector、deque、array等不同容器。
如果你熟悉C++的STL,再看Java的迭代器模式,会发现两者殊途同归——都是把"遍历"从数据结构中抽离出来。区别在于Java用对象接口表达,C++用模板和运算符重载表达。
8.3 Python开发者看迭代器模式:一切皆迭代
Python的迭代器模式和Java在思想上一致,但Python把迭代器概念语言化得更彻底。它的Iterable是实现了__iter__方法的对象,Iterator是实现了__next__方法的对象。任何实现了__iter__的对象都可以用于for循环,任何实现了__next__的对象都可以被next()函数调用。
Python的生成器(yield)更是把迭代器模式的实现成本降到了极致——一个包含yield的函数自动变成一个迭代器,不需要手动实现hasNext和next。这是迭代器模式在语言层面的增强表达。
如果你主要用Python,理解迭代器模式后可以把更多逻辑封装成生成器,用惰性求值处理大数据集,内存效率会有质的提升。
9. 迭代器模式的扩展思路和多Agent设计关联
9.1 从遍历数据到遍历服务的一步之遥
我在前面提到过多Agent系统的主从模式,这里展开讲讲。很多人在遇到Agent的sub-agent列表调度时,会不自觉地用for循环硬编码调用顺序。但当你把"调度"和"具体执行逻辑"解耦时,迭代器模式就是一种很好的抽象工具。
设想一个场景:主Agent要根据用户问题,按顺序调用多个子Agent(比如先做意图识别、再查知识库、最后生成回复)。这本质上是对子Agent列表的一次顺序遍历。如果你把这些子Agent包装成一个AgentPipeline类,并实现Iterable<Agent>接口,主逻辑只需要:
java复制for (Agent subAgent : agentPipeline) {
AgentResult result = subAgent.execute(context);
if (result.isDone()) {
return result;
}
}
这样有几个好处:子Agent列表的增删不影响主逻辑;可以中途根据条件终止遍历;可以按不同策略调整子Agent的调度顺序(定制不同迭代器)。这就是我前面提到的"另类工具调用"思路。
9.2 迭代器模式与主从模式的本质联系
主从Agent模式和迭代器模式的本质联系,在于两者都关注一件事:在不暴露内部复杂性的前提下,按照一定顺序处理一系列对象。
主Agent面对sub-agents,不用关心每个agent内部是怎么实现的,只需要有一个统一的"执行并返回结果"的接口。这和迭代器模式中的客户端面对Iterator接口完全同构。所以最新的智能体设计模式文档里,关于多Agent设计的章节,经常会把主从模式和迭代器模式并列讨论。
顺带提一句,网上有流传的"智能体设计模式PDF",里面关于Agent模式讲解得很细。但它在设计模式章节里对迭代器模式的讲解比较浅,只关注了数据遍历,没有延伸到服务遍历和行为遍历。大家看这类资料时,可以带着扩展思维去补充理解。
9.3 用迭代器模式优化Agent编排代码的实践
如果你在做Agent相关项目,可以尝试把Agent编排逻辑用迭代器模式重构一次。这里给出一个简化版思路:
java复制public interface Agent {
AgentResult execute(AgentContext context);
}
public class AgentPipeline implements Iterable<Agent> {
private List<Agent> agents = new ArrayList<>();
public void addAgent(Agent agent) {
agents.add(agent);
}
@Override
public Iterator<Agent> iterator() {
return agents.iterator();
}
}
调用方代码:
java复制AgentPipeline pipeline = new AgentPipeline();
pipeline.addAgent(new IntentAgent());
pipeline.addAgent(new KnowledgeAgent());
pipeline.addAgent(new ReplyAgent());
for (Agent agent : pipeline) {
AgentResult result = agent.execute(context);
if (result.isComplete()) {
return result;
}
}
如果你想让遍历的代码更符合"链式职责"的感觉,比如某个Agent处理完以后结果不能立即返回,而是传递给下一个Agent继续处理,这时可以定制一个特殊的迭代器,在next()里处理结果传递逻辑。这样调用方的代码基本不变,但其实遍历行为已经完全改变了。
10. 面试与考试中的迭代器模式高频考点
10.1 设计模式期末考试的重点
这里结合实际教学经验,给正在准备设计模式期末考的同学划个重点。迭代器模式在期末考试中反复出现的考点集中在:
- 迭代器模式属于哪一类模式?(行为型)
- 四个核心角色都有哪些?(Iterator、ConcreteIterator、Aggregate、ConcreteAggregate)
- 迭代器模式的优点:支持以不同方式遍历聚合对象、简化聚合类接口、支持多个遍历同时进行。
- 迭代器模式的缺点:增加类数量,对简单的聚合对象使用反而复杂化。
- 迭代器模式与for循环的区别:for循环依赖具体数据结构,迭代器不依赖具体数据结构。
- 什么是fail-fast机制?在JDK里的具体体现是什么?
- 增强for循环的底层实现是什么?为什么能用于自定义的
Iterable对象?
10.2 面试中关于迭代器的五连问
面试环节,迭代器模式出现的频率很高。我把面试官最常问的五个问题整理了一下,附上回答要点:
问题一:迭代器模式解决了什么问题?
答:核心是解耦遍历算法和数据结构。对客户端隐藏聚合对象内部结构,让客户端通过统一接口遍历元素。一套遍历代码,可用于不同数据结构。
问题二:modCount和expectedModCount的作用是什么?
答:实现fail-fast机制。集合在结构上被修改时modCount自增,迭代器保存创建时的expectedModCount。每次next()时比较两者,不一致说明迭代过程中集合被并发修改,立即抛出ConcurrentModificationException。
问题三:迭代器遍历时能删除元素吗?
答:可以用迭代器的remove()方法,但不能用集合自身的remove()。前者会同步更新expectedModCount,后者会导致两者不一致而抛异常。现代Java推荐用removeIf()。
问题四:能否同时多个迭代器遍历同一个集合?
答:可以。每个迭代器有自己的cursor和状态,互不干扰。这正体现了迭代器模式封装遍历状态的优点。
问题五:迭代器模式在JDK中有哪些应用?
答:ArrayList、LinkedList、HashSet、TreeSet等所有集合类都有对应的迭代器实现;Spliterator是迭代器模式的现代演进版,支撑并行流处理。
10.3 一道经典设计模式大作业:实现无损链表迭代器
很多设计模式大作业会让实现一个链表,并在链表上实现迭代器。这个题目之所以经典,是因为它考察的不只是迭代器本身,还有链表操作与迭代器状态的联动。
关键难点有两个:
第一个是remove()操作时链表的边界处理。删除头节点、中间节点、尾节点时,迭代器的cursor需要相应移动。删除的是当前节点时,cursor应该回退到前一个节点(或者指向下一个节点,取决于具体设计)。
第二个是在迭代过程中,链表自身的结构修改与迭代器状态的同步。如果链表在迭代中被修改了,迭代器怎么应对。这里又回到了fail-fast问题。
我当时做这个作业时,给出的实现思路是:链表持有一个modCount,迭代器持有expectedModCount。每次next()和remove()时先校验一致性。链表内部维护size和头尾节点。迭代器的cursor存储的是当前节点的引用,而不是下标,这样才能支持链表的O(1)插入和删除。
这个作业做好以后,你对迭代器模式的理解会非常扎实。
11. 一次完整的迭代器模式重构实战记录
11.1 重构前的代码:一坨if-else
我以实际工作中遇到的一个例子来做完整重构演示。假设有一个数据聚合接口,需要从三个数据源拿数据并合并展示。重构前的代码大致长这样:
java复制public List<String> getMergedData() {
List<String> result = new ArrayList<>();
// 数据源1:MySQL
List<String> dbData = databaseService.fetch();
for (String s : dbData) {
if (s != null && !s.trim().isEmpty()) {
result.add(s.trim());
}
}
// 数据源2:缓存
List<String> cacheData = cacheService.fetch();
for (String s : cacheData) {
if (s != null && !s.trim().isEmpty()) {
result.add(s.trim());
}
}
// 数据源3:远程接口
List<String> remoteData = remoteService.fetch();
for (String s : remoteData) {
if (s != null && !s.trim().isEmpty()) {
result.add(s.trim());
}
}
return result;
}
这个代码有三个明显的痛点:
- 三种数据源的遍历逻辑完全重复,只是数据来源不同。
- 如果新增数据源,又要复制粘贴一段循环,代码越来越膨胀。
- 过滤逻辑(去掉空白字符串)散落在各处,修改过滤规则要改多个地方。
11.2 应用迭代器模式后的设计
第一步:抽象数据源。
java复制public interface DataSource extends Iterable<String> {
String name();
}
第二步:为每个数据源实现各自类。
java复制public class DatabaseDataSource implements DataSource {
@Override
public String name() { return "MySQL"; }
@Override
public Iterator<String> iterator() {
List<String> data = databaseService.fetch();
return data.iterator();
}
}
缓存和远程接口类似,不再赘述。
第三步:统一聚合逻辑。
java复制public List<String> getMergedData() {
List<DataSource> sources = Arrays.asList(
new DatabaseDataSource(),
new CacheDataSource(),
new RemoteDataSource()
);
List<String> result = new ArrayList<>();
for (DataSource source : sources) {
for (String s : source) {
if (s != null && !s.trim().isEmpty()) {
result.add(s.trim());
}
}
}
return result;
}
11.3 这个重构到底改进了什么
看完重构前后的对比,有人可能会说"这不就是抽了个接口吗?本质上还是循环"。但差别恰恰体现在后续的维护上:
- 新增数据源:只要加一个实现
DataSource的类,然后在sources列表里加一行。核心聚合逻辑完全不动。 - 修改过滤规则:在聚合逻辑里改一处即可。如果你想把过滤规则下沉到每个数据源里,也可以在各数据源的迭代器里直接实现过滤(定制迭代器)。
- 调整数据源优先级:调整
sources列表的顺序即可。 - 每个数据源的遍历方式变化:比如数据库数据改为游标方式逐条读取,只需要改
DatabaseDataSource.iterator()的实现,聚合逻辑零修改。
这就是迭代器模式在工程实践中最真实的收益——不是让代码瞬间变快,而是让变更成本大幅下降。
12. 我对迭代器模式的一段使用体会
很早以前,我在代码里写遍历时,脑海里根本没有"设计模式"这个概念。后来在一次代码评审中,被老同事指出来:你为什么在高遍历频率、多数据源的模块里,直接用了ArrayList?如果数据源切了,你这段逻辑就得重写,为什么不定义一个迭代器接口?那次评审之后,我才开始认真研究迭代器模式。
照着书上的例子写了几个Demo,觉得不过是"把for循环包了一层"而已。直到后来做一个报表系统,数据源从MySQL切到ES再切到缓存,每次切换都需要改动调用方代码,我彻底意识到:当初写的那层"多此一举"的迭代器接口,恰恰是最省心的一层。
所以我的体会是:迭代器模式的第一价值从来不是性能,而是设计上的从容。你提前定义了一个遍历的边界,把"遍历什么"和"怎么遍历"分开,后续所有变更都变得可预期、可控。如果你还在为"这个集合到底该暴露什么结构"发愁,不妨先想想:调用方需要的是一个具体的集合,还是一种"能取到下一个元素"的能力?
用迭代器写出来的代码,通常都更安静、不张扬——它不关心数据从哪里来,也不关心数据到哪里去,只是安静地把你需要的元素一个接一个递给你。这种分工明确的感觉,也是我觉得设计模式最迷人的地方。
