迭代器模式:遍历的艺术

说实话,迭代器模式是我最早接触的设计模式之一。那时候我刚入行,写了一个自定义的集合类,结果发现遍历起来特别别扭——每次都要暴露内部数组,搞得我提心吊胆的。后来看到JDK源码里的Iterator接口,才恍然大悟:原来遍历这件事,可以做得这么优雅。

什么是迭代器模式?

迭代器模式的定义其实很简单:提供一种方法顺序访问一个聚合对象中的各个元素,而不暴露其内部表示

你想想看,我们平时遍历一个数组、一个列表,是不是直接用for循环就搞定了?但问题在于,如果这个集合不是数组,而是一个树结构、一个图结构,甚至是一个从数据库里分批读取的结果集呢?遍历方式就完全不一样了。

迭代器模式的核心思想,就是把「遍历」这个行为抽象出来。让集合类只负责存储数据,让迭代器负责遍历逻辑。各司其职,互不干扰。

核心要点:

  • 聚合对象(Aggregate):负责存储数据,提供创建迭代器的接口
  • 迭代器(Iterator):负责遍历逻辑,定义hasNext()、next()等方法
  • 客户端(Client):通过迭代器访问元素,不关心底层结构

为什么需要迭代器模式?

我在项目中遇到过这样一个场景:一个报表系统需要遍历多种数据源——有的来自内存列表,有的来自数据库游标,还有的来自第三方API的分页结果。如果每种数据源都暴露自己的遍历方式,调用方就得写一堆if-else分支。

迭代器模式的好处很明显:

  • 解耦遍历算法和集合结构:集合怎么存数据,迭代器怎么取数据,两者互不依赖
  • 支持多种遍历方式:同一个集合可以有不同的迭代器——正序遍历、逆序遍历、过滤遍历,随你定义
  • 简化客户端代码:调用方只需要调用hasNext()和next(),不用管底层是数组还是链表
  • 符合开闭原则:新增一种集合类型,只需要提供对应的迭代器,现有代码不用改

一个简单的例子

咱们来看一个最朴素的实现。假设我有一个书架,里面放了很多书。我想遍历这些书,但我不想暴露书架内部是用数组还是列表存的。

// 迭代器接口
interface Iterator<T> {
    boolean hasNext();
    T next();
}

// 聚合接口
interface Aggregate<T> {
    Iterator<T> createIterator();
}

// 书架 - 具体聚合类
class BookShelf implements Aggregate<Book> {
    private Book[] books;
    private int index = 0;

    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }

    public void addBook(Book book) {
        this.books[index] = book;
        index++;
    }

    @Override
    public Iterator<Book> createIterator() {
        return new BookShelfIterator(this);
    }
}

// 书架迭代器 - 具体迭代器类
class BookShelfIterator implements Iterator<Book> {
    private BookShelf bookShelf;
    private int index = 0;

    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
    }

    @Override
    public boolean hasNext() {
        return index < bookShelf.getLength();
    }

    @Override
    public Book next() {
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }
}

// 客户端使用
public class Client {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(5);
        bookShelf.addBook(new Book("设计模式"));
        bookShelf.addBook(new Book("重构"));
        bookShelf.addBook(new Book("代码整洁之道"));

        Iterator<Book> iterator = bookShelf.createIterator();
        while (iterator.hasNext()) {
            Book book = iterator.next();
            System.out.println(book.getName());
        }
    }
}

你看,客户端代码完全不知道BookShelf内部用的是数组。就算哪天我把数组改成ArrayList,只要迭代器接口不变,客户端一行代码都不用改。

迭代器模式的结构图

迭代器模式结构图 <<接口>> Iterator + hasNext(): boolean + next(): T ConcreteIterator - index: int + hasNext(): boolean + next(): T <<接口>> Aggregate + createIterator(): Iterator ConcreteAggregate - data: T[] + createIterator(): Iterator 创建 Client 使用迭代器遍历

迭代器模式在Java中的应用

其实Java标准库早就内置了迭代器模式。你每天都在用的ArrayList、HashSet,都实现了Iterable接口。这个接口要求返回一个Iterator对象。

我记得有一次排查性能问题,发现团队里有人用for循环遍历LinkedList。那个代码跑起来特别慢,因为LinkedList的get(index)是O(n)的。换成迭代器遍历后,性能直接提升了十几倍。

小提示:在Java中,增强for循环(for-each)本质上就是迭代器模式的语法糖。编译器会自动帮你生成调用iterator()和hasNext()、next()的代码。

// 这两种写法是等价的
// 写法一:增强for循环
for (String s : list) {
    System.out.println(s);
}

// 写法二:显式使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

迭代器模式的变体

迭代器模式不止一种玩法。我在实际项目中用过几种变体,分享给你:

变体类型 说明 适用场景
内部迭代器 迭代器自己控制遍历过程,客户端传入回调函数 需要对每个元素执行相同操作时
外部迭代器 客户端主动调用hasNext()和next()控制遍历 需要灵活控制遍历节奏时
双向迭代器 支持向前和向后遍历 需要来回查看元素时
过滤迭代器 只返回满足条件的元素 需要按条件筛选数据时
懒加载迭代器 按需加载数据,不一次性加载全部 处理大数据集或流式数据时

避坑指南

我曾经踩过一个坑:在遍历集合的同时,直接修改集合的结构(比如删除元素)。这会导致ConcurrentModificationException。正确的做法是使用迭代器自身的remove()方法,或者用CopyOnWriteArrayList这样的线程安全集合。

另外,迭代器模式也不是万能的。如果你的集合结构特别简单(比如固定大小的数组),直接用for循环反而更清晰。别为了用模式而用模式,这是很多新手容易犯的毛病。

什么时候用迭代器模式?

我个人习惯在以下几种场景中使用迭代器模式:

  • 集合的内部结构可能变化,但遍历方式需要保持稳定
  • 需要为同一个集合提供多种遍历方式
  • 遍历逻辑比较复杂,不想污染集合类的代码
  • 需要统一不同集合的遍历接口

说白了,迭代器模式就是给遍历这件事加了一层抽象。它让集合类专注于存储,让迭代器专注于遍历。各司其职,代码自然就清晰了。

嗯,关于迭代器模式就聊到这里。这个模式看起来简单,但用好了能让你的代码灵活不少。下次写自定义集合的时候,不妨试试给它配一个迭代器。


公众号:蓝海资料掘金营,微信deep3321