深入浅出设计模式【十六、迭代器模式】
一、迭代器模式介绍
在软件系统中,我们经常需要处理各种集合对象(如列表、树、图等)。这些集合的内部数据结构可能千差万别(数组、链表、哈希表、树),遍历它们的算法也随之不同。
如果客户端代码直接依赖这些集合的具体实现来进行遍历,会产生两个严重问题:
- 客户端代码与具体集合类高度耦合: 一旦需要更换集合类型,所有遍历该集合的客户端代码都需要修改。
- 客户端代码混乱: 同一份遍历逻辑会散落在各个客户端中,破坏了代码的复用性和清晰度。
迭代器模式通过将遍历集合的责任抽象到一个名为“迭代器”的对象中,完美地解决了上述问题。它为不同的集合提供统一的遍历接口,使得客户端可以以相同的方式遍历不同的集合,而无需关心集合背后的具体实现。
二、核心概念与意图
-
核心概念:
- 迭代器 (Iterator): 定义访问和遍历元素的接口。通常包含
hasNext(),next(),remove()等方法。 - 具体迭代器 (Concrete Iterator): 实现迭代器接口,并负责管理当前遍历的位置(游标)。
- 聚合 (Aggregate): 定义创建相应迭代器对象的接口。通常是一个
iterator()方法。 - 具体聚合 (Concrete Aggregate): 实现创建相应迭代器对象的接口,返回一个与该具体聚合对应的具体迭代器实例。
- 迭代器 (Iterator): 定义访问和遍历元素的接口。通常包含
-
意图:
- 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
- 将遍历元素的责任从聚合对象中分离出来,交给迭代器对象。这使得聚合对象可以更专注于数据存储,而迭代器专注于遍历,符合单一职责原则。
- 为不同的聚合结构提供统一的遍历接口,简化客户端代码。
三、适用场景剖析
迭代器模式在以下场景中非常有效:
- 需要以统一的方式遍历不同的集合结构时: 当系统需要处理多种数据结构(如列表、树、图),但希望客户端有一致的遍历体验时。
- 需要屏蔽聚合对象的复杂内部结构时: 当不希望暴露聚合对象的内部表示,只希望提供其元素的访问方式时。
- 需要支持聚合对象的多种遍历方式时: 例如,对同一个列表,可能需要前向遍历、后向遍历、跳序遍历等。使用迭代器模式可以轻松定义不同的迭代器来实现这些遍历,而无需修改聚合类本身。
- 为第三方聚合类提供统一访问接口时: 当使用一个无法修改源码的第三方集合类时,可以为其编写一个适配器迭代器,使其能融入统一的遍历体系中。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了迭代器模式的结构和角色间的关系:
Iterator(迭代器接口): 声明了遍历集合所需的基本操作,如hasNext()(是否还有下一个元素)、next()(获取下一个元素)、remove()(移除当前元素)。ConcreteIterator(具体迭代器):- 实现
Iterator接口。 - 持有对具体聚合对象的引用 (
-aggregate: ConcreteAggregate),以便访问其中的数据。 - 维护当前遍历的位置(游标,
-cursor: int)。 - 实现
hasNext()和next()方法,其逻辑依赖于底层聚合的具体数据结构(如数组、链表)。
- 实现
Aggregate(聚合接口): 定义了一个创建迭代器对象的工厂方法 (createIterator())。ConcreteAggregate(具体聚合):- 实现
Aggregate接口。 - 在
createIterator()方法中,返回一个与该聚合对应的具体迭代器实例(如new ConcreteIterator(this))。这通常被称为“工厂方法模式”的应用。 - 持有真正的数据集合 (
-elements: List~Element~)。
- 实现
Client(客户端):- 通过
Aggregate接口获取聚合对象。 - 通过聚合对象的
createIterator()方法获取其迭代器。 - 只依赖于
Iterator接口进行遍历,完全不知道底层聚合和具体迭代器的类型。
- 通过
五、各种实现方式及其优缺点
迭代器模式的实现主要有两种思想:外部迭代器和内部迭代器。
1. 外部迭代器 (External Iterator) - Java风格(主动控制)
客户端控制迭代过程,显式地调用 hasNext() 和 next() 方法来遍历每一个元素。
// 1. Iterator Interface (JDK-like)
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException("remove"); }
}
// 2. Aggregate Interface
public interface Iterable<T> {
Iterator<T> iterator();
}
// 3. Concrete Aggregate
public class MyList<T> implements Iterable<T> {
private List<T> elements = new ArrayList<>();
public void add(T item) { elements.add(item); }
@Override
public Iterator<T> iterator() {
return new MyListIterator(); // Factory method
}
// 4. Concrete Iterator (as an inner class, has access to 'elements')
private class MyListIterator implements Iterator<T> {
private int cursor = 0; // Current position
@Override
public boolean hasNext() {
return cursor < elements.size();
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return elements.get(cursor++);
}
}
}
// 5. Client
public class Client {
public static void main(String[] args) {
MyList<String> list = new MyList<>();
list.add("A");
list.add("B");
list.add("C");
// Get iterator and traverse (External Iteration)
Iterator<String> it = list.iterator();
while (it.hasNext()) { // Client controls the iteration
String element = it.next();
System.out.println(element);
}
// Alternatively, using for-each (syntax sugar for external iteration)
for (String s : list) { // This calls list.iterator() internally
System.out.println(s);
}
}
}
- 优点:
- 控制权在客户端,灵活性高。客户端可以决定何时获取下一个元素,甚至可以同时使用多个迭代器遍历同一个集合。
- 代码清晰: 遍历逻辑在客户端一目了然。
- 缺点:
- 客户端代码稍显冗长。
2. 内部迭代器 (Internal Iterator) - 函数式风格(被动接收)
迭代器控制迭代过程,客户端提供一个函数(回调函数),迭代器对集合中的每个元素应用这个函数。
// A simpler internal iterator interface
public interface InternalIterable<T> {
void forEach(Consumer<T> action);
}
public class MyList<T> implements InternalIterable<T> {
private List<T> elements = new ArrayList<>();
public void add(T item) { elements.add(item); }
@Override
public void forEach(Consumer<T> action) {
for (T element : elements) { // The iteration happens HERE, inside
action.accept(element); // Apply the client's function to each element
}
}
}
// Client
public class Client {
public static void main(String[] args) {
MyList<String> list = new MyList<>();
list.add("A");
list.add("B");
list.add("C");
// Internal Iteration: Client provides the action, collection does the looping
list.forEach(element -> System.out.println(element));
}
}
- 优点:
- 代码非常简洁。
- 将遍历逻辑封装在聚合内部,更易于实现复杂的并行遍历(如使用
parallelStream())。
- 缺点:
- 控制权在迭代器/聚合对象手中,客户端无法灵活控制遍历过程(如提前终止)。
Java的选择: Java集合框架主要采用外部迭代器(Iterator),因为它提供了最大的灵活性。同时,通过引入 Iterable 接口和增强的for循环(for-each),大大简化了外部迭代器的使用。Java 8引入的 forEach(Consumer) 方法(内部迭代)和Stream API,提供了内部迭代的选项。
六、最佳实践
- 优先使用现有迭代器: 永远不要自己实现迭代器模式! Java (
java.util.Iterator<E>) 和几乎所有现代语言的标准库都已经提供了完美且高效的迭代器实现。你的自定义集合类只需实现Iterable<T>接口即可。 - 让集合实现
Iterable接口: 这使得你的集合可以使用增强的for循环(for-each),这是最优雅的遍历方式。 - 考虑使用静态内部类实现迭代器: 如果必须为非常特殊的集合实现自定义迭代器,将其定义为该集合的一个静态内部类。这样可以访问集合的私有成员,同时保持代码的高内聚。
- 实现 fail-fast 行为: 一个好的迭代器应该是 fail-fast 的。如果在迭代过程中,集合被除了迭代器自身
remove()方法之外的方式修改了(如直接调用集合的add()或remove()),迭代器应该立即抛出ConcurrentModificationException,而不是冒着在将来某个不确定的时间发生任意不确定行为的风险。这是Java集合框架中迭代器的标准行为。 - 区分
Iterable和Iterator:Iterable: 表示“我可以提供迭代器”,是集合的抽象。只有一个方法iterator()。Iterator: 表示“我可以遍历”,是遍历过程的抽象。有hasNext(),next()等方法。- 这个区分使得一个集合可以同时提供多个不同的迭代器(如前向、后向)。
七、在开发中的演变和应用
迭代器模式的思想是现代编程语言和框架的基石,其应用已远远超出最初的设想:
- 集合框架的基石: 它是所有现代编程语言集合框架(Java Collections Framework, C++ STL, .NET LINQ)的核心设计模式,提供了遍历的统一抽象。
- 响应式编程与Stream API: Java 8的 Stream API 可以看作是一种更高级、更强大的“迭代器”。它代表了一个元素序列,但提供的不是逐个访问元素的方法,而是允许你声明性地描述对这些元素要执行的计算(如
filter,map,reduce)。它可以被视为内部迭代器的一种极致发展,并支持并行处理。 - 数据库访问: 数据库游标 (Cursor) 本质就是一个迭代器,它允许你在庞大的结果集中逐行(或逐批)移动,而无需将整个结果集加载到内存中。JDBC的
ResultSet对象就是一个迭代器。 - 目录遍历: 遍历文件系统目录树通常使用迭代器模式,返回文件/目录流,而不是一次性加载所有文件信息。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java Collections Framework -
java.util.Iterator&java.lang.Iterable:- 这是迭代器模式最经典、最普遍的应用。所有标准集合类(
ArrayList,HashSet,LinkedList,TreeSet等)都实现了Iterable接口。 - 客户端遍历时,无论是使用
while (it.hasNext())还是使用 for-each 循环,都在使用迭代器模式。
List<String> list = Arrays.asList("A", "B", "C"); // Example 1: Explicit Iterator (External) Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } // Example 2: Enhanced for-loop (External, syntax sugar) for (String s : list) { // This calls list.iterator() internally System.out.println(s); } // Example 3: Internal iteration with forEach (Java 8+) list.forEach(System.out::println); - 这是迭代器模式最经典、最普遍的应用。所有标准集合类(
-
Spring Framework -
org.springframework.util.MultiValueMap等:- Spring提供的许多集合工具类(如
MultiValueMap)也都实现了Iterable接口,可以无缝地融入Java的遍历生态中。
- Spring提供的许多集合工具类(如
-
JUnit 5 -
org.junit.jupiter.params.provider.ArgumentsProvider:- 在参数化测试中,你需要提供一个参数的集合。JUnit 5定义了一个
ArgumentsProvider接口,其核心方法provideArguments返回一个Stream<? extends Arguments>。这可以看作是一个高级的、流式的迭代器,用于提供测试参数流。
- 在参数化测试中,你需要提供一个参数的集合。JUnit 5定义了一个
-
Apache Commons Collections:
- 这个库提供了大量对Java集合的扩展,其中包括许多强大的、装饰性的迭代器,如:
FilterIterator: 只迭代满足条件的元素。TransformIterator: 在迭代过程中转换每个元素。ListIteratorWrapper: 适配器,将其他类型的迭代器适配成ListIterator。
- 这些是迭代器模式灵活性的绝佳证明。
- 这个库提供了大量对Java集合的扩展,其中包括许多强大的、装饰性的迭代器,如:
-
数据库访问 -
java.sql.ResultSet:ResultSet对象本质上就是一个迭代器。你调用next()方法来移动“游标”到下一行,然后使用getString(),getInt()等方法访问当前行的数据。- ORM框架(如Hibernate)的
Query.list()或Stream()方法,背后也是基于ResultSet的迭代。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 行为型设计模式 |
| 核心意图 | 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。 |
| 关键角色 | 迭代器(Iterator), 具体迭代器(ConcreteIterator), 聚合(Aggregate), 具体聚合(ConcreteAggregate) |
| 核心机制 | 1. 抽象遍历接口: 定义 hasNext() 和 next()。2. 工厂方法: 聚合通过 iterator() 方法创建迭代器。3. 游标管理: 迭代器内部维护遍历状态。 |
| 主要优点 | 1. 极大解耦: 解耦了集合结构与遍历算法,客户端只需面向接口编程。 2. 简化聚合接口: 聚合无需关心遍历逻辑。 3. 支持多种遍历: 可轻松为同一集合提供不同遍历方式。 4. 支持并行遍历: 可同时使用多个迭代器遍历同一集合。 |
| 主要缺点 | 1. 增加系统复杂性: 增加了一些类和接口。 2. 可能降低效率: 相比直接遍历,通过迭代器有一层间接调用。(通常可忽略不计) |
| 适用场景 | 几乎任何需要遍历集合的地方! 这是最常用的模式之一。 |
| 实现方式 | 外部迭代器 (主流, 灵活),内部迭代器 (简洁, 声明式)。 |
| 最佳实践 | 1. 使用语言内置迭代器 (如 java.util.Iterator)。2. 实现 Iterable 接口以支持for-each。3. 实现 fail-fast 迭代器。 |
| 现代应用 | 所有集合框架的基石,Stream API的思想先驱,数据库游标的概念模型。 |
| 真实案例 | Java Collections Framework (无处不在),JDBC ResultSet,Spring的各种集合工具。 |
迭代器模式是一个如此成功和基础的模式,以至于它已经完全融入了现代编程语言的血液中,成为了一种“无形的”基础设施。它的价值在于它极大地简化了客户端与集合交互的复杂性,并提供了无与伦比的灵活性和可复用性。理解迭代器模式,是理解现代API设计、集合框架和流处理的关键。它教导我们:如何优雅地遍历一个集合?答案是:“问集合要一个迭代器,然后用它。”
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120810

浙公网安备 33010602011771号