20、迭代器模式:统一遍历接口、Java中的Iterator

迭代器模式,说实话,是设计模式里最“润物细无声”的一个。

你每天都在用,但可能从来没意识到它的存在。Java里的IteratorIterable,还有那个增强for循环——底层全是它。我当年刚入行时,写了一个自定义集合类,遍历的时候直接暴露了内部数组。后来同事review代码,问我:“你为什么不返回一个Iterator?” 我当时一愣,心想“这有啥区别?” 结果后来那个集合被多个线程同时遍历,出了各种并发问题……嗯,从那以后,我再也不敢直接暴露内部数据结构了。

什么是迭代器模式?

说白了,就是提供一种方法,顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

你想想看,如果你写了一个自定义的列表,底层可能是数组,也可能是链表,甚至可能是树。调用方如果直接操作内部结构,那耦合度就太高了。迭代器模式就是在这中间加一层“遍历接口”,让调用方只关心“怎么拿下一个元素”,而不关心“元素到底存在哪”。

核心思想: 将遍历行为从聚合对象中分离出来,封装成一个独立的迭代器对象。

模式结构

迭代器模式其实就四个角色:

  • Iterator(抽象迭代器): 定义访问和遍历元素的接口,比如 hasNext()next()
  • ConcreteIterator(具体迭代器): 实现迭代器接口,维护遍历时的当前位置。
  • Aggregate(抽象聚合): 定义创建迭代器对象的接口,比如 createIterator()
  • ConcreteAggregate(具体聚合): 实现创建迭代器的方法,返回一个合适的迭代器实例。

我习惯把迭代器模式理解成“外卖小哥”。聚合对象就是餐厅后厨,你不需要知道菜是怎么炒的,你只需要一个外卖小哥(迭代器)帮你把菜一份一份送到你手上。你只管调用 next() 拿菜,后厨是数组还是炒锅,跟你没关系。

«interface» Iterator ConcreteIterator «interface» Aggregate ConcreteAggregate Client 使用

Java中的Iterator:你每天都在用

Java标准库早就把迭代器模式内置了。从JDK 1.2开始,java.util.Iterator 接口就存在了。后来JDK 1.5又引入了 Iterable 接口和增强for循环,让迭代器用起来更方便。

我举个例子,你肯定写过这样的代码:

List<String> list = new ArrayList<>();
list.add("Java");
list.add("设计模式");
list.add("迭代器");

// 增强for循环 —— 底层就是迭代器
for (String s : list) {
    System.out.println(s);
}

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

这两种写法效果完全一样。增强for循环只是语法糖,编译后会被转换成迭代器调用。我个人习惯在需要删除元素时用显式迭代器,因为增强for循环里不能直接调用 list.remove(),会抛 ConcurrentModificationException。但迭代器自己的 remove() 方法是安全的。

小技巧: 如果你在遍历时需要删除元素,记得用 iterator.remove(),而不是集合的 remove() 方法。这是很多新手踩过的坑。

手写一个迭代器:从零开始

光说不练假把式。我们来手写一个简单的迭代器,实现一个自定义的“书架”集合,可以遍历书架上的书。

// 1. 先定义书
class Book {
    private String name;
    public Book(String name) { this.name = name; }
    public String getName() { return name; }
}

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

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

// 4. 实现书架(具体聚合)
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);
    }

    public int getLength() {
        return index;
    }

    public Book getBookAt(int i) {
        return books[i];
    }
}

// 5. 实现书架迭代器(具体迭代器)
class BookShelfIterator implements Iterator<Book> {
    private BookShelf bookShelf;
    private int position = 0;

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

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

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

// 6. 客户端使用
public class Client {
    public static void main(String[] args) {
        BookShelf shelf = new BookShelf(5);
        shelf.addBook(new Book("设计模式之禅"));
        shelf.addBook(new Book("Java编程思想"));
        shelf.addBook(new Book("重构"));

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

你看,客户端代码完全不知道 BookShelf 内部用的是数组。如果哪天我想改成 ArrayList 或者数据库存储,只要迭代器接口不变,客户端一行代码都不用改。这就是迭代器模式最大的价值——解耦。

Java标准库中的迭代器

Java里迭代器的应用远不止 ListSet。我列几个常见的:

接口/类 说明 常见实现
java.util.Iterator 迭代器根接口 所有集合类的内部迭代器
java.util.ListIterator 支持双向遍历的迭代器 ArrayList.ListItr
java.util.Spliterator 可分割迭代器,用于并行流 ArrayList.ArrayListSpliterator
java.util.Enumeration 早期迭代器接口(已过时) VectorHashtable

我记得有一次做性能优化,需要并行处理一个超大的集合。普通迭代器是单线程的,一个一个遍历太慢。后来我换成了 Spliterator + 并行流,速度直接提升了4倍(4核机器)。Spliterator 可以把集合拆成多个子集,每个子集用一个线程处理。这就是迭代器模式在并发场景下的一个高级应用。

注意: 迭代器不是线程安全的。如果你在多线程环境下遍历同一个集合,一定要加锁或者使用 CopyOnWriteArrayList 这样的并发容器。我曾经在生产环境遇到过因为多线程同时遍历 ArrayList 导致的 ConcurrentModificationException,排查了半天才发现是迭代器的问题。

迭代器模式的优缺点

任何模式都有两面性,迭代器模式也不例外。

优点:

  • 支持以不同的方式遍历一个聚合对象。比如你可以实现正序遍历、倒序遍历、跳跃遍历等不同的迭代器。
  • 简化了聚合类的接口。遍历逻辑被抽出去了,聚合类只需要关注数据存储。
  • 符合开闭原则。增加新的聚合类和迭代器类,都不影响现有代码。

缺点:

  • 如果聚合对象很简单(比如只有一个数组),引入迭代器反而增加了代码复杂度。
  • 对于某些特殊的数据结构(比如图、树),迭代器的实现会比较复杂。

我个人觉得,迭代器模式最大的价值在于“抽象遍历”。你想想看,如果没有迭代器,每个集合类都要自己实现一套遍历方法,而且方法名还不一样——有的叫 next(),有的叫 getNext(),有的叫 fetch()……那调用方得多痛苦。迭代器统一了接口,让所有集合类的遍历方式变得一致。这就是设计模式的力量——用约定代替配置,用规范减少混乱。

避坑指南

我曾经在一个项目中,看到同事在迭代器遍历过程中直接修改了集合的结构(添加或删除元素),结果程序时不时抛异常。后来我帮他改成了使用 CopyOnWriteArrayList 或者用 ListIteratoradd()remove() 方法,问题才解决。

这里给大家几个建议:

  • 遍历过程中不要修改集合结构,除非你用的是迭代器自带的修改方法。
  • 如果需要在遍历时删除多个元素,可以考虑用 removeIf() 方法(JDK 8+),它内部已经处理好了并发问题。
  • 自定义迭代器时,一定要处理好 hasNext()next() 的边界条件,尤其是空集合的情况。

好了,关于迭代器模式,我们就聊到这里。记住一句话:迭代器模式的核心,就是“统一遍历接口,隐藏内部实现”。你以后写自定义集合类的时候,记得给它配一个迭代器——相信我,调用方会感谢你的。

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