迭代器模式(Iterator):遍历的优雅之道

说实话,迭代器模式是我在日常开发中用得最多、却最容易被忽视的模式之一。你想想看,每天写代码时,有多少次在遍历集合?for循环、while循环、增强for循环……但你真的思考过「遍历」这件事本身的设计吗?

我记得刚入行那会儿,有一次接手一个老项目。里面有个自定义的数据结构,存储了用户权限信息。为了遍历它,业务代码里到处是get(index)size()这样的调用。后来需求变了,底层存储从数组改成了链表,好家伙,改代码改到怀疑人生。那时候我就意识到——遍历逻辑不该和集合实现绑死。

这就是迭代器模式要解决的问题。

迭代器模式的结构

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

它的核心角色只有四个:

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

核心思想:将遍历行为从聚合对象中分离出来。聚合对象只负责存储数据,迭代器负责遍历数据。各司其职,互不干扰。

我习惯把迭代器模式理解成「导游模式」。你想想看,你去一个陌生的城市旅游,你不会自己瞎逛对吧?你会找个导游。导游知道怎么走、下一站去哪、还有没有景点没看。聚合对象就是那个城市,迭代器就是那个导游。

迭代器模式结构图 <<接口>> Iterator + hasNext(): boolean + next(): Object <<接口>> Aggregate + createIterator(): Iterator ConcreteIterator - currentIndex: int + hasNext(): boolean + next(): Object ConcreteAggregate - items: Object[] + createIterator(): Iterator 创建 虚线箭头:实现接口 | 实线箭头:创建关系

内部迭代器与外部迭代器

这里有个重要的概念区分——内部迭代器和外部迭代器。很多初学者容易搞混,我当年也踩过坑。

外部迭代器

外部迭代器,就是由客户端代码主动控制遍历过程。你调用一次next(),我就返回一个元素;你调用hasNext(),我就告诉你还有没有下一个。主动权在你手里。

// 外部迭代器示例
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    System.out.println(item);
    // 我可以在这里做各种控制:跳过、中断、甚至修改
    if ("stop".equals(item)) {
        break;  // 外部控制,想停就停
    }
}

外部迭代器的好处很明显:灵活。你可以控制遍历的节奏,可以提前终止,甚至可以同时维护多个迭代器遍历同一个集合。我在做数据分页处理时,就经常用外部迭代器来控制每次取多少条数据。

内部迭代器

内部迭代器就不一样了。你把一个「动作」交给迭代器,它自己从头到尾跑一遍,你插不上手。说白了,就是「你告诉我怎么处理每个元素,剩下的交给我」。

// 内部迭代器示例(Java 8 forEach)
list.forEach(item -> {
    System.out.println(item);
    // 注意:这里不能轻易中断遍历
    // 想 break?不好意思,做不到
});

内部迭代器的好处是代码简洁,尤其适合函数式编程风格。但缺点也很明显——你失去了控制权。我曾经在一个批处理任务里用了内部迭代器,结果处理到一半发现数据有问题,想停下来都停不了,只能等它全部跑完。嗯,那次之后我学乖了,需要控制的地方还是用外部迭代器。

对比维度 外部迭代器 内部迭代器
控制权 客户端控制 迭代器自身控制
灵活性 高(可中断、可跳过) 低(只能遍历到底)
代码简洁性 相对繁琐 非常简洁
多迭代器并发 支持 不支持
典型应用 Java Iterator Java forEach, JavaScript forEach

我的建议:如果你只是简单遍历所有元素做统一处理,用内部迭代器就够了。但如果你需要条件判断、提前终止、或者同时遍历多个集合,请选择外部迭代器。别为了省几行代码给自己挖坑。

迭代器模式在Java集合框架中的应用

Java集合框架是迭代器模式最经典的实践案例。你每天都在用,但可能没意识到它的设计有多巧妙。

Java的Collection接口继承了Iterable接口,而Iterable定义了iterator()方法,返回一个Iterator对象。这就是标准的迭代器模式结构。

// Iterable 接口(简化版)
public interface Iterable<T> {
    Iterator<T> iterator();
}

// Iterator 接口(简化版)
public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

你看,ArrayListLinkedListHashSetTreeSet……所有这些集合类都实现了Iterable接口。它们各自提供了自己的迭代器实现。

我记得有一次排查性能问题,发现代码里用for循环遍历LinkedList时用了get(i)的方式。结果呢?每次get(i)都要从头遍历到第i个元素,时间复杂度直接变成O(n²)。改成迭代器遍历后,瞬间就快了。这就是迭代器模式的价值——它让客户端代码和集合的实现解耦了。

// 错误示范:用索引遍历 LinkedList
LinkedList<String> list = new LinkedList<>();
// ... 添加大量数据
for (int i = 0; i < list.size(); i++) {
    String item = list.get(i);  // 每次都是 O(n) 操作!
}

// 正确示范:用迭代器遍历
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();  // 每次都是 O(1) 操作
}

// 或者用增强 for 循环(底层也是迭代器)
for (String item : list) {
    // 优雅且高效
}

我曾经踩过的坑:在遍历集合时直接调用list.remove()修改集合结构,结果抛出了ConcurrentModificationException。正确的做法是用迭代器的remove()方法,或者在遍历结束后再修改。记住:不要在迭代过程中修改集合的结构,除非你用的是迭代器自己的方法。

Java的ListIterator是迭代器模式的一个增强变体。它支持双向遍历,还能在遍历过程中添加和替换元素。说实话,这个接口在实际项目中用得不多,但当你需要的时候,它真的能救命。

ListIterator<String> listIt = list.listIterator();
// 正向遍历
while (listIt.hasNext()) {
    String item = listIt.next();
    if ("old".equals(item)) {
        listIt.set("new");  // 替换当前元素
    }
}
// 反向遍历
while (listIt.hasPrevious()) {
    String item = listIt.previous();
    System.out.println(item);
}

Java 8引入的Stream API,其实也借鉴了迭代器的思想。但Stream更偏向内部迭代器,它把遍历和操作都封装在流水线里了。你只需要声明「我要做什么」,而不需要关心「怎么遍历」。

// Stream 内部迭代
list.stream()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println);
// 遍历过程完全由 Stream 内部控制

说到这,我想起一个有意思的事。有一次面试,我问候选人「增强for循环和迭代器有什么区别?」他想了半天说「增强for循环更简洁」。没错,但底层原理呢?增强for循环其实就是语法糖,编译后还是生成迭代器代码。所以,理解迭代器模式,你才能真正理解Java集合遍历的本质。

总结一下:迭代器模式看似简单,但它体现了「关注点分离」这个重要的设计原则。把遍历逻辑从集合中抽离出来,让集合只关心数据存储,让迭代器只关心遍历方式。这种分离,让你的代码更灵活、更可维护。下次你写for (Item item : items)的时候,不妨想想——这背后,是一个优雅的设计模式在支撑着你。