20、迭代器模式:统一遍历接口、Java中的Iterator
迭代器模式,说实话,是设计模式里最“润物细无声”的一个。
你每天都在用,但可能从来没意识到它的存在。Java里的Iterator、Iterable,还有那个增强for循环——底层全是它。我当年刚入行时,写了一个自定义集合类,遍历的时候直接暴露了内部数组。后来同事review代码,问我:“你为什么不返回一个Iterator?” 我当时一愣,心想“这有啥区别?” 结果后来那个集合被多个线程同时遍历,出了各种并发问题……嗯,从那以后,我再也不敢直接暴露内部数据结构了。
什么是迭代器模式?
说白了,就是提供一种方法,顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
你想想看,如果你写了一个自定义的列表,底层可能是数组,也可能是链表,甚至可能是树。调用方如果直接操作内部结构,那耦合度就太高了。迭代器模式就是在这中间加一层“遍历接口”,让调用方只关心“怎么拿下一个元素”,而不关心“元素到底存在哪”。
模式结构
迭代器模式其实就四个角色:
- Iterator(抽象迭代器): 定义访问和遍历元素的接口,比如
hasNext()、next()。 - ConcreteIterator(具体迭代器): 实现迭代器接口,维护遍历时的当前位置。
- Aggregate(抽象聚合): 定义创建迭代器对象的接口,比如
createIterator()。 - ConcreteAggregate(具体聚合): 实现创建迭代器的方法,返回一个合适的迭代器实例。
我习惯把迭代器模式理解成“外卖小哥”。聚合对象就是餐厅后厨,你不需要知道菜是怎么炒的,你只需要一个外卖小哥(迭代器)帮你把菜一份一份送到你手上。你只管调用 next() 拿菜,后厨是数组还是炒锅,跟你没关系。
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里迭代器的应用远不止 List 和 Set。我列几个常见的:
| 接口/类 | 说明 | 常见实现 |
|---|---|---|
java.util.Iterator |
迭代器根接口 | 所有集合类的内部迭代器 |
java.util.ListIterator |
支持双向遍历的迭代器 | ArrayList.ListItr |
java.util.Spliterator |
可分割迭代器,用于并行流 | ArrayList.ArrayListSpliterator |
java.util.Enumeration |
早期迭代器接口(已过时) | Vector、Hashtable |
我记得有一次做性能优化,需要并行处理一个超大的集合。普通迭代器是单线程的,一个一个遍历太慢。后来我换成了 Spliterator + 并行流,速度直接提升了4倍(4核机器)。Spliterator 可以把集合拆成多个子集,每个子集用一个线程处理。这就是迭代器模式在并发场景下的一个高级应用。
CopyOnWriteArrayList 这样的并发容器。我曾经在生产环境遇到过因为多线程同时遍历 ArrayList 导致的 ConcurrentModificationException,排查了半天才发现是迭代器的问题。
迭代器模式的优缺点
任何模式都有两面性,迭代器模式也不例外。
优点:
- 支持以不同的方式遍历一个聚合对象。比如你可以实现正序遍历、倒序遍历、跳跃遍历等不同的迭代器。
- 简化了聚合类的接口。遍历逻辑被抽出去了,聚合类只需要关注数据存储。
- 符合开闭原则。增加新的聚合类和迭代器类,都不影响现有代码。
缺点:
- 如果聚合对象很简单(比如只有一个数组),引入迭代器反而增加了代码复杂度。
- 对于某些特殊的数据结构(比如图、树),迭代器的实现会比较复杂。
我个人觉得,迭代器模式最大的价值在于“抽象遍历”。你想想看,如果没有迭代器,每个集合类都要自己实现一套遍历方法,而且方法名还不一样——有的叫 next(),有的叫 getNext(),有的叫 fetch()……那调用方得多痛苦。迭代器统一了接口,让所有集合类的遍历方式变得一致。这就是设计模式的力量——用约定代替配置,用规范减少混乱。
避坑指南
我曾经在一个项目中,看到同事在迭代器遍历过程中直接修改了集合的结构(添加或删除元素),结果程序时不时抛异常。后来我帮他改成了使用 CopyOnWriteArrayList 或者用 ListIterator 的 add() 和 remove() 方法,问题才解决。
这里给大家几个建议:
- 遍历过程中不要修改集合结构,除非你用的是迭代器自带的修改方法。
- 如果需要在遍历时删除多个元素,可以考虑用
removeIf()方法(JDK 8+),它内部已经处理好了并发问题。 - 自定义迭代器时,一定要处理好
hasNext()和next()的边界条件,尤其是空集合的情况。
好了,关于迭代器模式,我们就聊到这里。记住一句话:迭代器模式的核心,就是“统一遍历接口,隐藏内部实现”。你以后写自定义集合类的时候,记得给它配一个迭代器——相信我,调用方会感谢你的。