迭代器模式:遍历的艺术
说实话,迭代器模式是我最早接触的设计模式之一。那时候我刚入行,写了一个自定义的集合类,结果发现遍历起来特别别扭——每次都要暴露内部数组,搞得我提心吊胆的。后来看到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,只要迭代器接口不变,客户端一行代码都不用改。
迭代器模式的结构图
迭代器模式在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