迭代器模式完全指南:从 for-each 到 Stream / Generator 的现代演化

迭代器模式是 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 的 ListIteratorIterator 多了 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 / 现代语言一般不暴露这种层次。

迭代器与并发

多线程迭代是个棘手问题。三种策略:

  1. fail-fast(ArrayList、HashMap):检测到并发修改就抛异常。安全但用着烦。
  2. fail-safe / weakly consistent(ConcurrentHashMap):迭代时看到的是某一时刻的快照,不保证看到后续修改,但不抛异常。
  3. copy-on-write(CopyOnWriteArrayList):写时整个数组复制,迭代器看到旧的不可变副本。读多写极少的场景适用。

识别需要自定义迭代器的场景

  1. 数据结构不是简单线性的(树、图、嵌套)。
  2. 遍历策略有多种(树的前/中/后/层序)。
  3. 数据量大或无限,需要惰性
  4. 遍历过程中需要状态(过滤、合并、变换)。

写在最后

迭代器模式成功到几乎隐形 —— 你天天用 for-each,却不会意识到背后是个 GoF 模式。这种"语言把它吸收进语法"的命运,是设计模式的最高荣誉。理解它的好处不只是"知道 for-each 怎么工作",而是意识到"遍历"本身可以是一种抽象:无限数据、惰性变换、流式管道、并行处理 —— 这些都是把"遍历"做成一等公民后才能实现的工程能力。

下次面对"我要遍历这个东西"的需求时,不必只想"for 循环怎么写"。问自己:它是不是惰性的?是不是无限的?要不要变换?要不要并行?把这些问题考虑进去,你写出来的代码会从"循环堆叠"升级到"流式管道",可读性、可组合性、性能都会跟着改进。这才是迭代器模式给的真正礼物。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

命令模式完全指南:从 Undo/Redo 到 Event Sourcing 与 CQRS

2026-5-15 15:29:20

技术教程

中介者模式完全指南:从表单联动地狱到消息总线的解耦利器

2026-5-15 15:29:20

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索