迭代器模式是 23 个 GoF 模式里最"成功"的一个 —— 成功到几乎所有现代语言都把它内置进语法:Java 的 for-each、Python 的 for x in、JS 的 for...of、C# 的 foreach、Rust 的 iter()。这背后都是迭代器模式。但内置归内置,什么时候要自己实现迭代器、惰性求值的代价、流式处理的边界,这些工程问题没那么显然。这篇文章把迭代器从最朴素的需求讲到 Stream / Generator / Rust Iterator 的现代演化。
问题:遍历集合不该泄露内部结构
看一个朴素的遍历:
// 内部用数组的集合
class IntArray {
int[] data;
int size;
}
// 客户端遍历
IntArray a = ...;
for (int i = 0; i < a.size; i++) {
int v = a.data[i];
process(v);
}
问题:客户端知道"内部是数组",于是写出依赖数组的循环。如果哪天 IntArray 改成链表实现,所有客户端代码都要改。
迭代器模式说:给集合提供一个"游标"对象,客户端只用游标的 hasNext / next 接口,不关心集合内部是数组、链表、树还是哈希表。
迭代器的标准接口
// 1. 迭代器接口
interface Iterator<T> {
boolean hasNext();
T next();
}
// 2. 可迭代对象接口
interface Iterable<T> {
Iterator<T> iterator();
}
// 3. 具体集合
class IntArrayList implements Iterable<Integer> {
private int[] data;
private int size;
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
int idx = 0;
public boolean hasNext() { return idx < size; }
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
return data[idx++];
}
};
}
}
// 客户端代码,完全不依赖内部
for (Integer x : list) process(x);
// 等价于
Iterator<Integer> it = list.iterator();
while (it.hasNext()) process(it.next());
关键认知:客户端代码对集合内部完全无感。换实现、加并发安全、加过滤,客户端代码一行不用改。
实战 1:遍历树结构
普通的 for-each 在树上不工作,但用迭代器封装"中序遍历"就行:
class TreeNode {
int value;
TreeNode left, right;
}
class BinaryTree implements Iterable<Integer> {
TreeNode root;
// 中序遍历的迭代器:用栈模拟递归
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
Deque<TreeNode> stack = new ArrayDeque<>();
{ pushLeft(root); } // 初始把左链压入
void pushLeft(TreeNode n) {
while (n != null) { stack.push(n); n = n.left; }
}
public boolean hasNext() { return !stack.isEmpty(); }
public Integer next() {
TreeNode n = stack.pop();
pushLeft(n.right); // 处理完当前节点,准备右子树
return n.value;
}
};
}
}
BinaryTree t = ...;
for (int v : t) System.out.println(v); // 自动中序遍历输出
客户端写得像在遍历数组,实际是在做树的中序遍历 —— 迭代器把递归算法藏起来。同样思路可以做层序遍历、前序、后序,实现不同但接口一致。
实战 2:文件目录的递归迭代
// Java NIO 内置,基于迭代器
try (Stream<Path> stream = Files.walk(Path.of("/tmp"))) {
stream.filter(p -> p.toString().endsWith(".log"))
.forEach(System.out::println);
}
// 自己写一个递归目录迭代器
class FileTreeIterator implements Iterator<File> {
private final Deque<Iterator<File>> stack = new ArrayDeque<>();
public FileTreeIterator(File root) {
if (root.isDirectory()) stack.push(Arrays.asList(root.listFiles()).iterator());
}
public boolean hasNext() {
while (!stack.isEmpty() && !stack.peek().hasNext()) stack.pop();
return !stack.isEmpty();
}
public File next() {
File f = stack.peek().next();
if (f.isDirectory()) stack.push(Arrays.asList(f.listFiles()).iterator());
return f;
}
}
惰性迭代器:迭代器最重要的能力
普通集合是"急切的":所有元素已经在内存里。迭代器可以是"惰性的":元素在 next() 时才生产。这意味着:
- 无限序列:斐波那契、自然数,内存只占 O(1)。
- 流式文件处理:几 GB 日志文件按行迭代,内存几 KB。
- 链式变换不落地:filter + map + take 一气呵成,中间不产生临时集合。
// Python 生成器:最自然的惰性迭代器
def naturals():
n = 0
while True: # 无限
yield n
n += 1
from itertools import islice
for x in islice(naturals(), 10): # 取前 10 个
print(x)
# Java Stream:惰性 + 函数式
IntStream.iterate(0, n -> n + 1) // 惰性的无限自然数
.filter(n -> n % 2 == 0) // 还没遍历,只是声明
.limit(10) // 限制 10 个
.forEach(System.out::println); // 这一步才真正驱动迭代
实战 3:大文件按行迭代
// 朴素读法:全部加载到 List
List<String> lines = Files.readAllLines(Path.of("huge.log"));
for (String l : lines) ...; // 几 GB 文件 OOM
// 迭代器读法:按需读取
try (Stream<String> stream = Files.lines(Path.of("huge.log"))) {
stream.filter(l -> l.contains("ERROR"))
.limit(100)
.forEach(System.out::println);
}
// 内部只在你需要下一行时才读一行,内存恒定
实战 4:数据库游标
数据库的 cursor 是迭代器在数据库层面的体现:
// JDBC ResultSet 就是个 Iterator
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM huge_table",
ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(1000); // 服务端游标,每次取 1000 行
ResultSet rs = stmt.executeQuery();
while (rs.next()) { // 迭代器风格
process(rs);
}
这种方式让你能流式处理几亿行数据,内存占用恒定。MongoDB 的 find().iterator()、ES 的 scroll API 都是同样思路。
JS Generator:语言级的迭代器
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) yield i;
}
for (const x of range(0, 10, 2)) console.log(x); // 0 2 4 6 8
// 无限生成器
function* fib() {
let [a, b] = [0, 1];
while (true) { yield a; [a, b] = [b, a + b]; }
}
const it = fib();
const first10 = Array.from({ length: 10 }, () => it.next().value);
// 自定义迭代器:实现 [Symbol.iterator]
class LinkedList {
[Symbol.iterator]() {
let node = this.head;
return {
next: () => node ? { value: (node = node.next).value, done: false }
: { done: true }
};
}
}
Rust 的 Iterator:零成本抽象
Rust 的迭代器是"懒 + 零成本"的标杆。所有 map / filter / take 链式调用最终被编译器内联成普通循环,运行时和手写 for 一样快:
let nums: Vec<i32> = (0..) // 无限自然数
.filter(|n| n % 2 == 0)
.map(|n| n * n)
.take(5)
.collect(); // 直到 collect 才真的迭代
// [0, 4, 16, 36, 64]
// 自定义迭代器:实现 Iterator trait
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
self.count += 1;
if self.count < 6 { Some(self.count) } else { None }
}
}
let total: u32 = Counter { count: 0 }
.map(|x| x * x)
.filter(|x| x % 3 == 0)
.sum();
// 编译后等价于 for 循环,无中间集合分配
外部迭代 vs 内部迭代
两种风格:
外部迭代:客户端控制
// 客户端拿到迭代器,自己决定何时调 next
Iterator<T> it = list.iterator();
while (it.hasNext()) {
T x = it.next();
if (badCondition) break; // 客户端可以提前停
if (skipCondition) continue;
process(x);
}
内部迭代:集合控制
// 客户端只给"处理函数",集合内部决定怎么遍历
list.forEach(x -> process(x));
list.stream().filter(...).map(...).forEach(...);
内部迭代的优势:并行化容易(stream.parallelStream())、更声明式;劣势:控制粒度细致时不如外部迭代灵活。简单遍历用内部,复杂控制流用外部。
迭代器的常见坑
坑 1:并发修改异常
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
for (Integer x : list) {
if (x == 3) list.remove(x); // ConcurrentModificationException
}
// 正确做法:用迭代器的 remove
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() == 3) it.remove();
}
// 或用 removeIf
list.removeIf(x -> x == 3);
原因:ArrayList 内部维护 modCount,迭代器检测到 modCount 变化就抛异常,防止你拿到坏数据。CopyOnWriteArrayList 等并发集合允许迭代时修改,但代价是写时复制。
坑 2:迭代器只能用一次
Stream<String> stream = list.stream();
stream.count(); // 消耗了
stream.forEach(...); // IllegalStateException
// 想多次用:每次新建 stream
list.stream().count();
list.stream().forEach(...);
Iterator 同样:遍历一遍就到末尾,再 hasNext 永远 false。需要重新遍历就重新调 iterator()。
坑 3:next() 越界
没先 hasNext 就 next,空集合会抛 NoSuchElementException。生产代码循环条件务必用 while (it.hasNext())。
坑 4:无限迭代器忘加终止条件
// Java
IntStream.iterate(0, n -> n + 1).forEach(System.out::println); // 永远不停!
IntStream.iterate(0, n -> n + 1).limit(10).forEach(...); // 加 limit
双向迭代器、随机访问迭代器
Java 的 ListIterator 比 Iterator 多了 hasPrevious / previous / add / set:
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("b")) it.set("B"); // 替换当前元素
if (s.equals("a")) it.add("A1"); // 在 a 后插入
}
C++ STL 把迭代器分得更细:输入、输出、前向、双向、随机访问。每一级支持更多操作,但需要容器支持。这是模板编程的基础,但 Java / 现代语言一般不暴露这种层次。
迭代器与并发
多线程迭代是个棘手问题。三种策略:
- fail-fast(ArrayList、HashMap):检测到并发修改就抛异常。安全但用着烦。
- fail-safe / weakly consistent(ConcurrentHashMap):迭代时看到的是某一时刻的快照,不保证看到后续修改,但不抛异常。
- copy-on-write(CopyOnWriteArrayList):写时整个数组复制,迭代器看到旧的不可变副本。读多写极少的场景适用。
识别需要自定义迭代器的场景
- 数据结构不是简单线性的(树、图、嵌套)。
- 遍历策略有多种(树的前/中/后/层序)。
- 数据量大或无限,需要惰性。
- 遍历过程中需要状态(过滤、合并、变换)。
写在最后
迭代器模式成功到几乎隐形 —— 你天天用 for-each,却不会意识到背后是个 GoF 模式。这种"语言把它吸收进语法"的命运,是设计模式的最高荣誉。理解它的好处不只是"知道 for-each 怎么工作",而是意识到"遍历"本身可以是一种抽象:无限数据、惰性变换、流式管道、并行处理 —— 这些都是把"遍历"做成一等公民后才能实现的工程能力。
下次面对"我要遍历这个东西"的需求时,不必只想"for 循环怎么写"。问自己:它是不是惰性的?是不是无限的?要不要变换?要不要并行?把这些问题考虑进去,你写出来的代码会从"循环堆叠"升级到"流式管道",可读性、可组合性、性能都会跟着改进。这才是迭代器模式给的真正礼物。
—— 别看了 · 2026