foreach 语法糖背后,迭代器模式做了多少脏活
你天天写 for (String s : list),觉得理所当然。但你有没有想过,为什么数组可以用 foreach,ArrayList 可以用,HashSet 也可以用,甚至你自己写的类也能用 foreach 遍历?
背后全是迭代器模式(Iterator Pattern)在干活。
为什么需要迭代器
假设你有一个商品列表,客户端代码需要遍历它。最直接的方式:
java
ArrayList<String> products = new ArrayList<>();
// ... 加数据 ...
for (int i = 0; i < products.size(); i++) {
System.out.println(products.get(i));
}
这段代码没问题,直到产品经理说"这个列表要改成用 Set 存储,去重"。
你得把所有遍历代码从索引遍历改成迭代器遍历,或者更惨------改成用 toArray() 转成数组再遍历。如果你的列表在十个地方被遍历过,恭喜你,十个地方都要改。
迭代器模式要解决的问题就一个:把"怎么遍历"和"遍历什么"解耦。
集合负责存数据,迭代器负责遍历逻辑。你换集合实现,遍历代码不用动一行。
迭代器的标准结构
经典的迭代器模式就两个接口:
java
// 迭代器接口
public interface Iterator<E> {
boolean hasNext();
E next();
}
// 聚合接口------拥有迭代能力的集合
public interface Aggregate<E> {
Iterator<E> iterator();
}
Java 的 Collection 接口继承了 Iterable,iterator() 方法返回一个 Iterator 实例------就是上面这个模式的标准实现。
ArrayList 的迭代器大概长这样(简化版):
java
public class ArrayListIterator<E> implements Iterator<E> {
private final ArrayList<E> list;
private int cursor = 0;
public ArrayListIterator(ArrayList<E> list) {
this.list = list;
}
@Override
public boolean hasNext() {
return cursor < list.size();
}
@Override
public E next() {
return list.get(cursor++);
}
}
就这?对,核心逻辑真的就这么简单。hasNext() 判断还有没有下一个,next() 取出当前元素并移到下一个位置。
foreach 语法糖是怎么用上迭代器的
Java 编译器在编译 for (String s : list) 的时候,会自动把它转成迭代器调用:
java
// 你写的
for (String s : list) {
System.out.println(s);
}
// 编译器实际生成的
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
这就是为什么任何实现了 Iterable 接口的类都能用 foreach 。你不需要做任何额外的事,只要你的类有个 iterator() 方法返回 Iterator 就行。
自己写一个迭代器
来看一个实际场景。假设你做了一个日志系统,日志存在一个自定义的数据结构里:
java
public class LogBuffer {
private LogEntry[] entries = new LogEntry[100];
private int count = 0;
public void append(LogEntry entry) {
if (count < entries.length) {
entries[count++] = entry;
}
}
}
外部要遍历日志,怎么办?直接暴露 entries 数组?那调用方就得知道你内部用数组存的数据,耦合就来了。
加个迭代器:
java
public class LogBufferIterator implements Iterator<LogEntry> {
private final LogEntry[] entries;
private final int size;
private int cursor = 0;
public LogBufferIterator(LogEntry[] entries, int size) {
this.entries = entries;
this.size = size;
}
@Override
public boolean hasNext() {
return cursor < size;
}
@Override
public LogEntry next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return entries[cursor++];
}
}
让 LogBuffer 实现 Iterable:
java
public class LogBuffer implements Iterable<LogEntry> {
// ...
@Override
public Iterator<LogEntry> iterator() {
return new LogBufferIterator(entries, count);
}
}
现在外部代码可以直接用 foreach 了:
java
for (LogEntry entry : logBuffer) {
System.out.println(entry.getMessage());
}
将来你把内部数据结构从数组改成链表或者跳表,外部代码一行不用改。这就是迭代器模式的价值。
一个容易踩的坑:并发修改
写过迭代器的人大概率都踩过这个坑:
java
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) {
list.remove(s); // ConcurrentModificationException!
}
}
跑一下就知道,抛 ConcurrentModificationException。原因很简单:foreach 背后是迭代器在遍历,你在遍历过程中修改了集合的结构,迭代器的状态就乱了。
ArrayList 的迭代器里有一个 expectedModCount 字段,每次调用 next() 之前都会检查它和集合的 modCount 是否一致。remove() 操作会修改 modCount,但不会更新迭代器的 expectedModCount,所以检查失败直接抛异常。
解决办法是用迭代器自己的 remove() 方法:
java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) {
it.remove(); // 安全,迭代器内部会同步 expectedModCount
}
}
或者 Java 8 以后用 removeIf():
java
list.removeIf(s -> s.equals("b"));
Java 的迭代器演进
迭代器模式在 Java 里经历了好几轮演进:
Java 1.0 :Enumeration 接口,只有 hasMoreElements() 和 nextElement(),不支持删除。Hashtable、Vector 用的就是这个。
Java 1.2 :引入 Iterator,加了 remove() 方法,Enumeration 被标记为过时。整个 Collections 框架基于迭代器模式重建。
Java 5 :引入 Iterable + foreach 语法糖,迭代器使用变得更优雅。
Java 8 :引入 Iterator.forEachRemaining() 和 Spliterator,支持并行遍历。Stream 的底层就是基于 Spliterator 工作的。
每一次演进都在让迭代器更好用,但核心思想始终没变------把遍历行为从集合内部抽离出来。
什么时候该自己实现迭代器
说实话,日常开发中你很少需要自己写一个全新的迭代器。Java 的集合框架已经覆盖了绝大多数场景。
但你会在这些情况下需要自定义迭代器:
- 自定义数据结构:比如树形结构的前序/中序/后序遍历、跳表遍历、B+ 树叶子节点遍历
- 特殊遍历逻辑:比如跳过某些元素、按优先级遍历、只遍历满足条件的元素
- 懒加载遍历:数据量大到不能一次性加载到内存,需要按需读取(类似数据库游标)
最后一个场景特别有意思。假设你要遍历一个 100 万行的 CSV 文件,你不可能全部读进内存再遍历。你可以做一个逐行读取的迭代器:
java
public class CsvLineIterator implements Iterator<String[]> {
private final BufferedReader reader;
private String[] nextLine;
public CsvLineIterator(BufferedReader reader) {
this.reader = reader;
advance();
}
private void advance() {
try {
String line = reader.readLine();
nextLine = line != null ? line.split(",") : null;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public boolean hasNext() {
return nextLine != null;
}
@Override
public String[] next() {
if (!hasNext()) throw new NoSuchElementException();
String[] result = nextLine;
advance();
return result;
}
}
调用方拿到的就是一个普通的迭代器,根本不知道底层是逐行读文件还是全部加载到了内存。这种透明性就是迭代器模式的精髓。
迭代器模式看起来简单,但它可能是你用得最多却最不会注意到的一个设计模式。下次你写 foreach 的时候,可以想想背后那位默默干活的 Iterator。
我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章------每篇文章我都会同步对应的小程序内容。