走进 Gang of Four 设计模式:迭代器模式

说明:不仅告诉你"怎么用",更告诉你"为什么这样设计"

前置知识:Java 面向对象基础(接口、集合框架基础)、for-each 语法糖


📊 设计模式分类总览

GoF 23 种设计模式可按两个维度交叉分类:类/对象(处理方式) × 创建型/结构型/行为型(目的)。

维度 创建型(Creational) 结构型(Structural) 行为型(Behavioral)
类(Class)
(通过继承复用)
Factory Method
工厂方法
Adapter(类适配器) Interpreter(解释器)
Template Method(模板方法)
对象(Object)
(通过组合/聚合复用)
Abstract Factory(抽象工厂)
Builder(建造者)
Prototype(原型)
Singleton(单例)
Adapter(对象适配器)
Bridge(桥接)
Composite(组合)
Decorator(装饰)
Facade(外观)
Flyweight(享元)
Proxy(代理)
Chain of Resp.(责任链)
Command(命令)
Iterator(迭代器)
Mediator(中介者)
Memento(备忘录)
Observer(观察者)
State(状态)
Strategy(策略)
Visitor(访问者)

📑 目录

  1. 模式概述 · 15 问深度分析
  2. 框架源码实战分析
  3. 深度追问:Why / How / Trade-off / Evolution / Modern Practice
  4. 总结
  5. 参考文献

1. 模式概述 · 15 问深度分析

Q1: 为什么需要这个模式?它解决了什么问题?

在软件开发中,我们经常需要处理各种数据集合(如列表、树、图、哈希表等)。

它解决的核心问题: 如何在不暴露集合内部复杂结构的前提下,统一、顺序地访问集合中的元素。

如果没有迭代器,每当集合的内部数据结构发生变化(例如从数组改为链表),所有遍历该集合的业务代码全部都需要重写。迭代器模式通过提供一个标准接口,屏蔽了底层数据结构的差异。

Q2: 如果不用这个模式,会有什么缺陷?

  1. 破坏封装性:客户端必须直接了解集合的内部实现(例如,必须知道它是 ArrayList 的数组形式,还是 LinkedList 的链表节点形式),才能写出遍历代码。
  2. 集合类职责过重:集合既要负责"数据的高效存储与增删",又要负责"各种顺序的遍历逻辑",违反了单一职责原则。
  3. 无法应对复杂并发/多重遍历:如果集合自己维护一个遍历指针,同一时间只能有一个线程或一段代码对其进行遍历。

Q3: 核心思想是什么?一句话如何概括?

  • 核心思想:将集合的"数据存储"与"数据遍历"职责分离,用一个独立的"迭代器对象"来跟踪遍历进度。
  • 一句话概括“将遍历行为抽离为独立对象,让客户端以统一的接口按序访问各类集合,而不必关心其底层结构。”

Q4: 包含哪些角色?每个角色的职责是什么?

角色 职责
Iterator(抽象迭代器) 定义访问和遍历元素的接口,通常包含 hasNext()next()
ConcreteIterator(具体迭代器) 实现抽象迭代器,记录当前遍历位置,持有具体集合的引用
Aggregate / Iterable(抽象聚合类) 定义创建迭代器的接口(如 iterator()
ConcreteAggregate(具体聚合类) 实现抽象聚合类,返回相应的具体迭代器实例,存储实际数据

Q5: 它们之间如何协作?调用流程是怎样的?

  1. 创建阶段:客户端持有具体聚合类对象,调用其 iterator() 方法。
  2. 实例化:聚合类在方法内部 new 出一个具体迭代器对象(通常作为内部类),将自身指针或数据传给迭代器。
  3. 遍历阶段:客户端通过抽象迭代器接口循环:先调用 hasNext() 判断,若有则调用 next() 获取元素,迭代器内部的指针向后移动一位。

Q6: 为什么要这样设计?每个角色存在的意义是什么?

  • 抽象层(Iterator & Aggregate)的存在意义:实现了面向接口编程。客户端只需要知道 IterableIterator 两个接口,就能遍历任何复杂的集合。
  • 具体聚合类的意义:专心管理数据的生命周期和存储结构。
  • 具体迭代器的意义:作为一个"外挂的指针箱",持有遍历所需的游标状态。因为它是独立的,所以你可以为同一个集合创建 N 个迭代器同时进行 N 个独立遍历。

Q7: 为什么使用接口、抽象类、组合,而不是其他方式?

  • 为什么用接口/抽象类:为了实现多态遍历。如果不使用接口,客户端代码就必须绑定到具体的迭代器实现上。
  • 为什么用组合(迭代器持有集合的引用):迭代器必须能够获取集合中的数据。通过内部类机制,具体迭代器可以在不破坏集合封装的前提下,安全地读取集合内部的私有数组或链表节点。

Q8: 这种设计符合哪些面向对象原则?

  • 单一职责原则 (SRP):集合只负责数据存储,迭代器只负责数据遍历。
  • 开闭原则 (OCP):如果引入了一种全新的数据结构,只需实现一个新的迭代器即可,客户端的遍历代码不需要修改。
  • 依赖倒置原则 (DIP):客户端代码不依赖具体的集合或迭代器实现类,而是依赖于 IterableIterator 高层抽象接口。

Q9: 它有哪些优点和局限性?会带来哪些额外成本?

优点:

  • 支持以不同的方式遍历同一个集合(正向、反向、过滤遍历)。
  • 简化了聚合类,简化了客户端调用。
  • 在同一个集合上可以同时进行多个遍历。

局限性与额外成本:

  • 类数量成倍增加:每个集合类都需要配对一个具体迭代器类。
  • 额外内存与性能开销:每次遍历都需要创建新的迭代器对象。
  • Fail-Fast 成本:需要额外的状态维护(如 modCount)。

Q10: 它最适合哪些场景?哪些场景不应该使用?

最适合的场景:

  • 需要访问一个聚合对象的内容,而不需要暴露它的内部表示。
  • 需要为聚合对象提供多种遍历方式(如深度优先、广度优先、逆序等)。
  • 需要为不同的聚合结构提供一个统一的遍历接口。

不应该使用的场景:

  • 简单的线性数组:直接用 for(int i=0; i<len; i++) 性能最优。
  • 数据流或一次性读取:不适合反复遍历的迭代器模式。

Q11: 它与容易混淆的其他设计模式有什么区别和联系?

与访问者模式 (Visitor) 的区别:

  • 迭代器模式只能顺序、逐个地获取元素,操作由客户端控制。
  • 访问者模式用于对结构复杂的复合对象中的不同类型元素执行不同操作,由结构内部导向(Double Dispatch)。

与组合模式 (Composite) 的联系:

  • 组合模式用于表示"部分-整体"的层次结构。遍历组合模式的树状结构时,通常会结合迭代器模式来实现深度优先或广度优先遍历。

Q12: 框架中有哪些典型应用?

  • Java/JDKjava.util.Iterablejava.util.Iterator 接口。所有集合框架(ArrayListHashSet 等)都实现了它。Java 5+ 的 foreach 语法糖本质上就是迭代器。
  • Spring FrameworkCompositeIterator——将多个迭代器合并为一个。
  • MyBatisCursor 接口。在大数据量查询时,selectCursor 返回一个 Cursor 对象,允许通过迭代的方式一条条从数据库拉取数据,避免内存溢出。
  • TomcatNamingEnumeration 等内部命名空间迭代器。

Q13: 如何从零实现一个最小可运行版本?

// 1. 抽象迭代器
interface MyIterator<T> {
    boolean hasNext();
    T next();
}

// 2. 抽象聚合类
interface MyList<T> {
    MyIterator<T> iterator();
    void add(T element);
}

// 3. 具体聚合类
class MyArrayList<T> implements MyList<T> {
    private Object[] elements = new Object[10];
    private int size = 0;

    public void add(T element) {
        if (size < elements.length) {
            elements[size++] = element;
        }
    }

    @Override
    public MyIterator<T> iterator() {
        return new MyArrayListIterator();
    }

    // 4. 具体迭代器(内部类)
    private class MyArrayListIterator implements MyIterator<T> {
        private int cursor = 0;

        @Override
        public boolean hasNext() {
            return cursor < size;
        }

        @SuppressWarnings("unchecked")
        @Override
        public T next() {
            if (!hasNext()) throw new RuntimeException("No more elements");
            return (T) elements[cursor++];
        }
    }
}

// 使用
MyList<String> names = new MyArrayList<>();
names.add("Alice"); names.add("Bob"); names.add("Charlie");

MyIterator<String> it = names.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

Q14: 如何在现有项目中识别可以使用该模式的地方?

  1. 暴露了底层容器:发现某个 Service 类中大量出现 List<Item> list = order.getItems(),然后针对这个 list 进行 for 循环。
  2. 重复的遍历逻辑:不同的业务类在不同地方,都用复杂的条件遍历同一个自定义树形结构或图结构。
  3. 切换数据结构引发雪崩:团队想把某个核心对象的存储从 Map 改为 List,结果发现修改引发上百个类的编译报错。

Q15: 如果重新设计这个模式,在今天会有哪些改进?

  1. 全面拥抱 Stream API:今天更倾向于直接将集合暴露为 Stream<T>,支持 filtermapreduce,并可开启 .parallelStream() 实现并行遍历。
  2. 响应式流(Reactive Streams / Flux):在微服务和异步高并发场景下,传统的迭代器是阻塞式的。现代设计使用 Flux,变成响应式(Push)迭代。
  3. 默认方法(Default Methods)增强:可以直接在 Iterator 接口中内置 forEachRemaining(Consumer) 等默认方法。

2. 框架源码实战分析

2.1 JDK ArrayList 内部类 Itr 与 Fail-Fast 机制

JDK 接口定义:

// java.lang.Iterable
public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

// java.util.Iterator
public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() { throw new UnsupportedOperationException("remove"); }
}

ArrayList 内部类 Itr 实现与 Fail-Fast 机制:

public class ArrayList<E> extends AbstractList<E> implements List<E> {
    private class Itr implements Iterator<E> {
        int cursor;       // 下一个要返回的元素的索引
        int lastRet = -1; // 上一个返回的元素的索引
        int expectedModCount = modCount; // 快照当前的修改次数

        Itr() {}

        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];
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException(); // Fail-Fast
        }
    }
}

细节剖析(Fail-Fast 机制): ArrayList 包含成员变量 modCount(修改计数器)。当调用 list.add()list.remove() 时,modCount++。创建迭代器时,Itr 内部记录 expectedModCount = modCount。如果遍历期间外部代码直接修改了集合,导致 modCount 改变,迭代器在执行 next() 时通过 checkForComodification() 发现两值不相等,立刻抛出 ConcurrentModificationException

foreach 语法糖的真面目:

// 原始代码
for (String s : list) { System.out.println(s); }

// 编译后
Iterator var2 = list.iterator();
while(var2.hasNext()) {
    String s = (String)var2.next();
    System.out.println(s);
}

这就是为什么在 foreach 循环里直接调用 list.remove() 会报错,而必须使用 iterator.remove() 的原因(因为 iterator.remove() 内部会同步更新 expectedModCount = modCount)。

2.2 Spring CompositeIterator 组合迭代器

Spring 在处理多个不同来源的配置源时,将"组合模式"和"迭代器模式"融合,写出了 CompositeIterator

核心源码:

package org.springframework.util;

public class CompositeIterator<E> implements Iterator<E> {
    private final Set<Iterator<E>> iterators = new LinkedHashSet<>();
    private boolean inUse = false;

    public void add(Iterator<E> iterator) {
        Assert.state(!this.inUse, "You cannot add iterators once enumeration has begun");
        if (this.iterators.contains(iterator)) return;
        this.iterators.add(iterator);
    }

    @Override
    public boolean hasNext() {
        this.inUse = true;
        for (Iterator<E> iterator : this.iterators) {
            if (iterator.hasNext()) return true;
        }
        return false;
    }

    @Override
    public E next() {
        this.inUse = true;
        for (Iterator<E> iterator : this.iterators) {
            if (iterator.hasNext()) return iterator.next();
        }
        throw new NoSuchElementException("All iterators have been exhausted");
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("CompositeIterator does not support remove()");
    }
}

细节探讨:对于调用方而言,它只知道自己拿到了一个普通的 Iterator,不断 next()。而实际上,Spring 可能把来自 ClassPathFileSystem、系统环境变量的迭代器缝合在一起。这就是接口隔离与多态遍历带来的最大好处。

2.3 MyBatis Cursor 流式查询游标

在海量数据查询时,如果直接调用 session.selectList(),MyBatis 会把整条 SQL 查出的数据一次性全部加载到 JVM 内存,极易导致 OOM。Cursor 接口的本质就是一个面向数据库结果集的迭代器。

Cursor 接口定义:

package org.apache.ibatis.cursor;

public interface Cursor<T> extends Closeable, Iterable<T> {
    boolean isOpen();
    boolean isConsumed();
    int getCurrentIndex();
}

DefaultCursor 内部的具体迭代器实现:

private class ObjectWrapperIterator implements Iterator<T> {
    private T object;
    private int iteratorIndex = -1;

    @Override
    public boolean hasNext() {
        if (object == null) {
            object = fetchNextObject(); // 按需去数据库连接中捞一条数据
        }
        return object != null;
    }

    @Override
    public T next() {
        T next = object;
        if (next == null) {
            next = fetchNextObject();
        }
        if (next != null) {
            object = null; // 消费后清空,为下一条腾出空间
            iteratorIndex++;
            return next;
        }
        throw new NoSuchElementException();
    }
}

细节探讨(如何不爆内存): 当你使用 cursor.iterator() 遍历时,MyBatis 并不是一次性把数据读完。每调用一次 hasNext()/next(),底层的 fetchNextObject() 才驱使 JDBC 驱动的 ResultSet.next() 向数据库前进一步,拿到一条数据。上一条对象失去引用后及时被 GC 回收,使内存占用始终保持在一个极低的恒定水平

使用约束:因为数据是实时从数据库连接里拉取的,所以遍历 Cursor 必须在同一个数据库事务(@Transactional)内进行。

2.4 Tomcat NamingEnumeration 与封装迭代器

Tomcat 内部管理大量 JNDI 资源,为了让上层架构能用标准方式遍历资源,大量使用了老版本的迭代器变种 Enumeration

Tomcat 常见的适配器设计:

// Tomcat 内部把现代 Iterator 包装成旧版 Enumeration
public class EnumerationIterator<E> implements Enumeration<E> {
    private final Iterator<E> iterator;

    public EnumerationIterator(Iterator<E> iterator) {
        this.iterator = iterator;
    }

    @Override
    public boolean hasMoreElements() {
        return iterator.hasNext();
    }

    @Override
    public E nextElement() {
        return iterator.next();
    }
}

细节探讨(组件隔离): Tomcat 作为底层应用服务器,核心原则之一是高封装性。JNDI 中注册了哪些真实的类,绝对不能向业务 Web 应用公开。通过统一返回 NamingEnumeration 迭代器,Web 开发者只能通过 nextElement() 挨个拿到配置项,而永远无法窥探 Tomcat 底层的数据结构。

四者对比总结:

框架 迭代器角色 解决的问题 核心机制
JDK ArrayList Itr 内部类 统一集合遍历接口 Fail-Fast 通过 modCount 保证安全
Spring CompositeIterator CompositeIterator 合并多个迭代器 内部持有 Set<Iterator> 轮询遍历
MyBatis Cursor ObjectWrapperIterator 大数据量按需加载 fetchNextObject() 单条拉取
Tomcat NamingEnumeration EnumerationIterator 隐藏底层存储结构 适配器模式包装 Iterator 为 Enumeration

3. 深度追问

3.1 Why(为什么):解决哪个根本矛盾?

迭代器模式解决的根本矛盾是:"集合的多样性与复杂性"与"客户端对高层抽象与一致性遍历"之间的矛盾。

具体拆解为两个维度的冲突:

  • 数据存储与行为控制的矛盾:如果把遍历逻辑也写在集合类里,集合类的职责就会迅速爆棚,违反单一职责原则。
  • 信息封装与透明公开的矛盾:客户端需要获取集合里的每个元素,但集合类又必须隐藏其内部实现细节。

迭代器模式就像一扇"单向旋转门"——它既不破坏集合的绝对封装,又给外部提供了一条能且仅能"顺着往前走"的唯一通道。

3.2 How(怎么做):通过哪些对象关系和交互机制?

通过将静态的关系转化为动态的、解耦的交互来解决问题。

对象关系:

  • 客户端不直接依赖具体集合,也不依赖具体迭代器。
  • 客户端只依赖两个高层抽象:Iterable 接口和 Iterator 接口。
  • 具体迭代器(通常作为内部类)持有具体集合的引用。

交互机制:

  1. 控制权转移(工厂方法):客户端不自己 new 迭代器,而是调用集合的 iterator() 方法。
  2. 状态持有(游标指针):迭代器内部维持 int cursorNode current,这个状态不属于集合
  3. 协作消费:客户端 while(iterator.hasNext()) → 迭代器根据自身状态去具体集合取数 → 通过 next() 返回。

3.3 Trade-off(代价):牺牲了什么换取什么?

牺牲(付出代价) 换取(获得收益)
类的数量翻倍:每个集合都要对应一个迭代器类 完美的封装性:集合底层从数组换成红黑树,客户端代码不需要改
运行期性能与 GC 开销:每次遍历都要 new Itr() 并发多重遍历:同一时刻多个线程可对同一个集合进行独立遍历
Fail-Fast 复杂度:必须引入 modCount 等版本控制 统一的 API 规范:全行业只需学会 hasNext() + next()

3.4 Evolution(演化):如何从简单设计逐步演化而来?

  1. 萌芽期(原始指针/索引):最早只有 for (int i=0; i<len; i++),强依赖线性连续物理内存。
  2. 标准化(GoF 经典迭代器):1994 年《设计模式》将其确立,区分了内部迭代器和外部迭代器。
  3. 语言级支持(Java Collection / foreach):Java 5+ 引入 foreach 语法糖。

衍生变体:

  • 双向迭代器(ListIterator):允许向前 (next()) 和向后 (previous()) 双向移动。
  • 过滤/条件迭代器(FilterIterator):在 next() 时自动跳过不符合条件的元素。
  • 安全迭代器(Snapshot Iterator):创建迭代器时克隆集合快照,遍历时原集合被修改也不报错(如 CopyOnWriteArrayList)。

3.5 Modern Practice(现代实践):在今天是否被取代?

结论:经典"外表"在被逐渐取代,但"灵魂"升华到了更高维度。

被替代的经典外表:

  • 函数式编程与 Stream APIlist.stream().filter(x -> x > 10).map(String::valueOf).forEach(System.out::println)——Stream 本质上是内部迭代器,把"如何遍历"的控制权全权交给了 JVM。

在响应式(Reactive)中的升华:

  • Spring WebFlux / Project ReactorFlux<T>Mono<T>。传统迭代器是拉(Pull)模式(客户端向集合要数据);现代响应式是推(Push)模式(服务端主动推送)。
  • 但这依然是迭代器思想的延续——响应式流的底层依然保留了背压(Backpressure)机制,消费者可以告诉生产者"我还能消费多少条",这本质上就是由消费者控制游标的分布式迭代器。

总结:你不再需要自己写 Iterator 类,但迭代器模式作为集合与算法解耦的理论基石,已经化身为了 Stream、Cursor、Flux 和 Channel,成为了现代高并发与流处理系统的底层血脉。


4. 总结

核心要点

  • 迭代器模式将"遍历"从聚合中抽离,统一通过 hasNext()/next() 接口访问元素
  • Fail-Fast 机制modCount 版本控制保证遍历安全性
  • 四个工业级变体:JDK ArrayList(遍历安全)、Spring CompositeIterator(多迭代器合并)、MyBatis Cursor(流式加载)、Tomcat EnumerationIterator(接口适配)
  • 现代演进:Stream API(函数式)、Flux/Reactor(响应式推模式)

一句话

迭代器模式把"怎么存"和"怎么遍历"分开——你只管 next() 取下一个,不用管底层是数组还是链表、是内存还是数据库游标。


5. 参考文献

[1] GAMMA E, HELM R, JOHNSON R, et al. Design Patterns: Elements of Reusable Object-Oriented Software[M]. Boston: Addison-Wesley, 1994.
[2] Oracle. Java Collections Framework[EB/OL]. https://docs.oracle.com/javase/tutorial/collections/, 2024.
[3] MyBatis. MyBatis 3 Cursor[EB/OL]. https://mybatis.org/mybatis-3/, 2024.
[4] SPRING. Spring CompositeIterator[EB/OL]. https://docs.spring.io/spring-framework/, 2024.
[5] Apache Tomcat. Tomcat JNDI[EB/OL]. https://tomcat.apache.org/, 2024.

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐