1. 前言
在日常编程中,我们几乎无时无刻不在与"集合"打交道:数组、列表、哈希表、树形结构...这些集合承载着我们的数据。而处理数据最常见的操作之一,就是遍历------逐个访问集合中的每个元素。
试想一下,如果没有一种统一的遍历方式,我们会面临什么?对于数组,我们得用 for (int i = 0; i < array.length; i++)
;对于链表,我们可能需要 while (node != null)
;而对于更复杂的树或图,遍历逻辑就更加复杂多样了。当业务代码需要处理不同类型的集合时,就必须了解它们各自的内置结构,这导致了代码的高度耦合和复杂性。
Iterator(迭代器)模式正是为了解决这个问题而生的。它旨在提供一种统一的方法来顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。简单来说,Iterator 模式将遍历的逻辑从集合本身中分离出来,交给一个名为"迭代器"的对象负责。
2. 定义
Iterator 模式是一种行为设计模式,它定义了一个接口,用于顺序访问集合对象中的元素,而无需了解其底层的实现细节。该模式通常涉及两个核心角色:
- 迭代器接口: 定义了遍历集合所需的基本操作,通常包含:
next()
: 返回集合中的下一个元素。hasNext()
: 判断是否还有更多元素可以遍历。- (可选)
remove()
: 从集合中移除当前元素。
- 可迭代集合: 提供一个方法(通常称为
iterator()
)用于获取一个指向集合开头的迭代器对象。
text
迭代器模式的核心思想是:将对聚合对象的访问和遍历职责分离,从而允许聚合专注于数据存储,而迭代器专注于遍历算法。这体现了单一职责原则,并通过抽象接口化解了聚合与遍历操作之间的静态耦合。
3. 应用
Iterator 模式的应用场景非常广泛,几乎在所有现代编程语言的标准库中都有它的身影。
3.1 场景一:统一遍历不同集合
这是最经典的场景,假设有一个方法需要打印所有元素,无论传入的是 ArrayList
还是 HashSet
,你都可以使用相同的 Iterator 接口。
java
// 定义一个打印方法
public void printAll(Iterator<String> iterator) {
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
}
// 使用
List<String> list = Arrays.asList("A", "B", "C");
Set<String> set = new HashSet<>(Arrays.asList("X", "Y", "Z"));
printAll(list.iterator()); // 遍历列表
printAll(set.iterator()); // 遍历集合,代码无需任何改动!
3.2 场景二:简化语法(增强型for循环)
Java 的 for-each
循环和 Python 的 for ... in
循环,其底层实现就是 Iterator 模式。编译器会自动将这种语法糖转换为使用迭代器的代码。
java
// 语法糖(开发者这样写)
for (String item : list) {
System.out.println(item);
}
// 编译器处理后(实际执行)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
}
3.3 场景三:支持多种遍历方式
一个复杂的集合(如一棵树)可以同时提供多种迭代器,例如"前序遍历迭代器"、"中序遍历迭代器"和"广度优先遍历迭代器"。客户端可以根据需要选择不同的迭代器,而不需要修改集合类本身。
假设我们有一个简单的二叉树结构:
java
// 二叉树节点
class TreeNode {
String value;
TreeNode left;
TreeNode right;
TreeNode(String value) { this.value = value; }
}
// 二叉树集合类
class BinaryTree {
TreeNode root;
// ...
// 提供不同的迭代器
public Iterator<String> getPreOrderIterator() {
return new PreOrderIterator(root);
}
public Iterator<String> getInOrderIterator() {
return new InOrderIterator(root);
}
public Iterator<String> getLevelOrderIterator() {
return new LevelOrderIterator(root);
}
}
下面给出前序遍历迭代器的参考实现:
java
class PreOrderIterator implements Iterator<String> {
private Stack<TreeNode> stack = new Stack<>();
public PreOrderIterator(TreeNode root) {
if (root != null) stack.push(root);
}
@Override
public boolean hasNext() {
return !stack.isEmpty();
}
@Override
public String next() {
if (!hasNext()) throw new NoSuchElementException();
TreeNode node = stack.pop();
// 注意入栈顺序:先右后左,保证出栈时是左先于右
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
return node.value;
}
}
3.4 场景四:延迟遍历或无限集合
迭代器的"按需计算"(或称"懒加载")特性使其在处理海量数据流或理论上无限的序列时极具价值。它允许我们创建一个逻辑上的集合,而无需在内存中实现存储所有元素。
示例1:海量数据分页遍历 假设需要从数据库或网络中分批读取大量数据,迭代器可以隐藏分页获取的细节。
java
// 一个模拟从数据库分页读取的迭代器
class PaginatedDataIterator implements Iterator<String> {
private int currentPage = 0;
private int pageSize = 100;
private Iterator<String> currentPageData;
private List<String> fetchPageFromDatabase(int page) {
// 模拟数据库查询:SELECT ... LIMIT pageSize OFFSET page*pageSize
// 这里返回模拟数据
List<String> data = new ArrayList<>();
for (int i = 0; i < pageSize; i++) {
data.add("Item-" + (page * pageSize + i));
}
return data;
}
@Override
public boolean hasNext() {
// 如果当前页数据为空或已遍历完,则尝试获取下一页
if (currentPageData == null || !currentPageData.hasNext()) {
List<String> newPage = fetchPageFromDatabase(currentPage);
if (newPage.isEmpty()) {
return false; // 没有更多数据了
}
currentPageData = newPage.iterator();
currentPage++;
}
return true;
}
@Override
public String next() {
if (!hasNext()) throw new NoSuchElementException();
return currentPageData.next();
}
}
// 使用:客户端可以像遍历普通集合一样处理海量数据
Iterator<String> massiveDataIterator = new PaginatedDataIterator();
while (massiveDataIterator.hasNext()) {
String item = massiveDataIterator.next();
process(item); // 每次只在内存中保留一小批数据
if (someCondition) break; // 可以随时中断,避免不必要的加载
}
示例2:无限序列生成器 迭代器可以代表一个永不结束的序列,元素只在被请求时才生成。
java
// 生成无限斐波那契数列的迭代器
class FibonacciIterator implements Iterator<BigInteger> {
private BigInteger a = BigInteger.ZERO;
private BigInteger b = BigInteger.ONE;
@Override
public boolean hasNext() {
return true; // 总是有下一个元素,因为序列是无限的
}
@Override
public BigInteger next() {
BigInteger nextVal = a;
// 更新状态为下一个数
a = b;
b = nextVal.add(b);
return nextVal;
}
}
// 使用:可以按需获取序列中的值,而不必(也无法)预先计算所有值
Iterator<BigInteger> fibIterator = new FibonacciIterator();
for (int i = 0; i < 100; i++) { // 只取前100个,尽管序列是无限的
System.out.println(fibIterator.next());
}
// 或者与Stream等惰性求值机制结合使用(在Java中)
// Stream.generate(() -> fibIterator.next()).limit(10).forEach(...);
4. 开源代码解析
4.1 顶层设计:Iterator
与 Iterable
接口
- 分离关注点 :
Iterable
表示"可迭代的",职责是提供一个迭代器。Iterator
表示"迭代器",职责是控制遍历过程。这种分离使得一个集合可以同时提供多种遍历方式(如TreeSet
可以提供升序和降序迭代器)。
java
// java.util.Iterator
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
// JDK 8+ 新增:对剩余元素执行操作,常用于函数式编程
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
// java.lang.Iterable
public interface Iterable<T> {
Iterator<T> iterator();
// JDK 8+ 新增的默认方法,支持forEach循环
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
// 并行流相关,暂不深入
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
4.2 ArrayList.Itr
:快速失败(Fail-Fast)机制的典范
ArrayList
的迭代器是理解 Java 迭代器核心机制的最佳起点。它是一个私有内部类,从而可以无缝访问外部类的所有成员。
java
// 在 ArrayList 类内部
private class Itr implements Iterator<E> {
int cursor; // 下一个要返回元素的索引,初始为0
int lastRet = -1; // 最后一个返回元素的索引;如果没有元素则返回-1(用于remove操作)
int expectedModCount = modCount; // 关键!初始化时记录外部ArrayList的修改次数
Itr() {} // 构造函数
public boolean hasNext() {
return cursor != size; // 简单比较游标和集合大小
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification(); // 【关键点1】检查是否发生并发修改
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData; // 获取外部类的数组引用
if (i >= elementData.length)
throw new ConcurrentModificationException(); // 【关键点2】另一种检查
cursor = i + 1;
return (E) elementData[lastRet = i]; // 返回元素,并记录lastRet
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); // 检查并发修改
try {
ArrayList.this.remove(lastRet); // 调用外部类的remove方法
cursor = lastRet; // 游标回退,因为后面的元素都前移了一位
lastRet = -1; // 重置lastRet,防止连续remove
expectedModCount = modCount; // 【关键点3】remove后,更新expectedModCount!
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// 核心检查方法:快速失败(Fail-Fast)的灵魂
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
深度解析:
- 快速失败(Fail-Fast)机制 :
modCount
是ArrayList
的一个成员变量,任何会结构性修改 集合的操作(如add
,remove
,clear
)都会使modCount++
。- 迭代器在创建时捕获当前的
modCount
值,存为expectedModCount
。 - 在每次迭代操作(
next
,remove
)前,都会检查modCount != expectedModCount
。如果不相等,说明在迭代过程中集合被外部修改了,立即抛出ConcurrentModificationException
。 - 设计意图:尽早发现并发修改的bug,避免出现不确定的行为(比如无限循环、跳过元素、重复元素)。
remove
方法的特殊处理 :- 迭代器自己的
remove
方法是唯一"合法"的在迭代中修改集合的方式。 - 在成功调用
ArrayList.this.remove(lastRet)
后,它会主动将expectedModCount
更新为新的modCount
。这样,迭代器"认可"这次修改,不会抛出异常。 - 这解释了为什么用
for-each
循环(底层使用迭代器)时直接调用list.remove(item)
会抛异常,而使用iterator.remove()
则不会。
- 迭代器自己的
4.3 HashMap
的迭代器:应对散列冲突的遍历
HashMap
的迭代更复杂,因为它需要遍历"数组 + 链表/红黑树"。其迭代器是 HashIterator
抽象类,KeyIterator
, ValueIterator
, EntryIterator
都是它的简单派生类。
java
// 在 HashMap 类内部
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的条目
Node<K,V> current; // 当前条目
int expectedModCount; // 用于快速失败
int index; // 当前所在的哈希桶索引
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
// 初始化:找到第一个非空的桶(bucket)
if (t != null && size > 0) {
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
// 关键:如何找到下一个元素
if ((next = (current = e).next) == null && (t = table) != null) {
// 如果当前链表/树下没有下一个节点,则寻找下一个非空的桶
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(key, null, false, true); // 调用HashMap的移除方法
expectedModCount = modCount; // 更新
}
}
深度解析:
- 遍历顺序与不确定性 :
HashMap
迭代器的遍历顺序是桶(bucket)的顺序 ,从table[0]
到table[table.length-1]
。对于每个桶,再遍历其中的链表或红黑树。- 这个顺序不是 插入顺序,也不是 键的排序顺序。它取决于哈希值、桶的大小和冲突解决策略。因此,不要依赖
HashMap
的迭代顺序。
- 性能考量 :
- 迭代性能与
HashMap
的size
(元素数量)和capacity
(桶数组大小)有关。一个具有较大容量但元素很少的HashMap
(负载因子低),迭代起来可能比元素数量相同的ArrayList
慢,因为它需要跳过很多空桶。 LinkedHashMap
通过维护一个贯穿所有条目的双向链表,提供了可预测的迭代顺序(通常是插入顺序或访问顺序),解决了这个问题,但牺牲了一些空间和插入/删除性能。
- 迭代性能与
4.4 并发场景下的迭代器:CopyOnWriteArrayList
ArrayList
的快速失败机制并不保证真正的线程安全,它只是"尽力而为"地抛出异常来提醒你。真正的并发安全集合,比如 CopyOnWriteArrayList
,采用了完全不同的策略。
java
// CopyOnWriteArrayList 的迭代器
public Iterator<E> iterator() {
// 直接返回底层数组当前快照(snapshot)的迭代器!
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
// 迭代器持有创建时数组的快照引用
private final Object[] snapshot;
private int cursor;
COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements; // 注意:这里只是引用赋值,但外部数组是volatile且写时复制
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public E next() {
if (!hasNext()) throw new NoSuchElementException();
// 所有操作都在快照数组上进行,这个数组在迭代器生命周期内永远不会变
return (E) snapshot[cursor++];
}
// ... 其他方法,remove/set/add 通常不支持,直接抛异常
}
深度解析:
- 原理 :当需要修改集合时(如
add
,set
),不直接在原数组上操作,而是将原数组复制一份,在副本上进行修改,修改完成后,再将集合的引用指向新的数组。 - 迭代器行为 :迭代器持有的是创建那一刻的数组快照。在迭代器生命周期内,无论其他线程如何修改集合(增删改),迭代器遍历的都是那个"旧的"快照。
- 优势 :迭代操作永远不需要加锁,永远不会抛出
ConcurrentModificationException
,性能极高。 - 代价 :
- 内存开销:写操作会导致数组复制,内存占用大。
- 数据延迟 :迭代器看不到迭代开始后发生的修改,这种特性称为弱一致性。这可能是优点也可能是缺点,取决于业务场景。
5. 总结
迭代器模式不仅仅是为了"方便遍历"。其核心是一种解耦思想,属于行为型设计模式。
- 职责分离 :
- 聚合对象(集合) :核心职责是高效地存储和管理数据。它不应该关心"如何遍历"这些数据。
- 迭代器对象 :核心职责是定义和执行遍历算法。它封装了遍历所需的游标、顺序等状态和逻辑。
- 价值 :这使得集合和遍历算法可以独立地变化和扩展。你可以轻松地为同一个集合增加多种遍历方式(如正序、逆序、广度优先),而无需修改集合本身的代码,符合开闭原则。
- 抽象接口 :
- 模式通过统一的
Iterator
接口,将遍历这个行为抽象出来。客户端代码只依赖于这个抽象接口,而不是具体的集合实现,从而降低了耦合度。
- 模式通过统一的
- 封装复杂性 :
- 对于复杂数据结构(如树、图),遍历逻辑可能非常复杂。迭代器将这些复杂性隐藏在内部,为客户端提供一个简单的
next()
和hasNext()
接口,简化了客户端的使用。
- 对于复杂数据结构(如树、图),遍历逻辑可能非常复杂。迭代器将这些复杂性隐藏在内部,为客户端提供一个简单的
Java 根据集合的不同特性,实现了多种迭代器策略,体现了权衡的艺术。
集合类型 | 迭代器策略 | 特点 | 适用场景 |
---|---|---|---|
ArrayList , HashMap |
快速失败 | 性能好,直接访问底层数组/链表。严禁在迭代中被外部修改。 | 单线程环境,或能确保迭代期间无修改的场景。 |
CopyOnWriteArrayList |
弱一致性 | 基于创建时的数据快照 进行迭代。迭代过程中集合的修改对迭代器不可见。永不抛 ConcurrentModificationException 。 |
读多写极少的高并发场景。牺牲写性能(复制数组)和内存,换取读的高性能和无锁遍历。 |
LinkedHashMap |
顺序迭代 | 通过维护一个双向链表,提供了可预测的迭代顺序(插入顺序或访问顺序)。 | 需要按特定顺序遍历 Map 的场景。 |
ConcurrentHashMap |
弱一致性 | 迭代器反映的是创建时或之后某个点的状态。可以容忍并发修改,不会抛出异常,但不保证能迭代到所有创建后新加入的元素。 | 高并发读写场景。在安全性和一致性之间取得平衡。 |
在下一篇《【基础数据篇】数据访问守卫:Accessor模式》中,我们将探讨如何优雅地控制对对象属性的读写,封装数据访问逻辑,保障数据安全。