写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
别急着说「迭代器不就是 foreach 吗」。这篇文章聊的是迭代器模式里那些你每天都踩、但从不自知的坑。
前两天帮同事看一个线上 Bug,日志里躺着:
php
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)
他一脸困惑:「我就遍历的时候删了个元素,怎么就崩了?」
我说你先别急着改,我们来聊聊这个异常到底在保护你什么。聊完之后他沉默了------十年 Java 开发,从没想过 foreach 底下那一层到底在干什么。
你在用迭代器,但你不认识它
Java 的 for-each 语法糖把你保护得太好了。写 for (User u : users) 的人,十个里有八个不知道这行代码编译后长什么样:
java
// 你写的
for (User u : users) {
System.out.println(u.getName());
}
// 编译器帮你生成的
Iterator<User> it = users.iterator();
while (it.hasNext()) {
User u = it.next();
System.out.println(u.getName());
}
对大多数人来说,迭代器就是个透明的东西------知道它存在,但从来没正眼看过它。有事的时候 ConcurrentModificationException 甩你一脸,没事的时候就当它不存在。
但迭代器模式远不止 foreach 语法糖这点事。
fail-fast 不是 Bug,是设计
回到开头的 ConcurrentModificationException。ArrayList 的迭代器里有一个叫 modCount 的字段,记录集合被结构性修改的次数。每次 add()、remove(),modCount 加一。迭代器创建时把当前的 modCount 存成 expectedModCount,每次 next() 或 remove() 先检查两者是否一致。
这个机制叫 fail-fast,设计目的是尽早暴露并发修改的问题,而不是让它悄无声息地产生错误结果。
问题是,很多人不理解这个设计意图,在 StackOverflow 上搜到的「解决方案」是:
java
for (User u : new ArrayList<>(users)) {
if (u.getAge() < 18) {
users.remove(u); // 不会再崩了------因为你遍历的是副本
}
}
这能跑,但在一个百万级的列表上这样写就是灾难。你遍历一次,O(n);你复制一次,又 O(n);你删除一次(ArrayList 的 remove 是 O(n)),整体复杂度直接上天。
正确的做法就三种,看你的场景选:
java
// 方案一:迭代器自己的 remove
Iterator<User> it = users.iterator();
while (it.hasNext()) {
if (it.next().getAge() < 18) {
it.remove(); // 不会触发 ConcurrentModificationException
}
}
// 方案二:Java 8 removeIf
users.removeIf(u -> u.getAge() < 18);
// 方案三:流式操作,生成新列表(适合不可变场景)
List<User> adults = users.stream()
.filter(u -> u.getAge() >= 18)
.collect(Collectors.toList());
三种方案对应三种场景:方案一适合需要精细控制遍历过程的场景,方案二最简洁,方案三适合函数式风格和不希望修改原集合的场景。很多人只会方案三,但方案三在只需要删除几个元素的时候多了一次完整复制,反而是浪费。
迭代器模式真正狠的地方:统一遍历接口
上面说的这些,都还只是 Java 集合框架里的冰山一角。迭代器模式真正的价值在于它把「遍历」这个行为从「数据结构」中抽离出来了。
ArrayList 底层是数组,LinkedList 底层是链表,TreeSet 底层是红黑树。三种完全不同的数据结构,遍历方式天差地别。但你用迭代器的时候:
java
Iterator<String> it = collection.iterator();
while (it.hasNext()) {
String s = it.next();
}
不管是数组、链表还是红黑树,你用同一套代码走遍天下。这就是迭代器模式的核心抽象:提供一种方法顺序访问聚合对象中的各个元素,而不暴露其内部表示。
没有这个抽象,遍历 ArrayList 你得用 for (int i = 0; i < list.size(); i++),遍历 LinkedList 同样用这个写法就是灾难------因为 LinkedList 的 get(i) 是 O(n) 的。
别笑,真有人在 LinkedList 上写 for-i 循环。后果就是------功能上没问题,性能上直接退化到 O(n²)。
自定义迭代器:不是炫技,是真能干活的
Java 的 Iterable 接口就一个方法:
java
public interface Iterable<T> {
Iterator<T> iterator();
}
实现它,你的类就能用 foreach。但好人用迭代器一般不只是为了 foreach。
比如你有一个树形结构------目录树、组织架构树、菜单树------每次需要「遍历所有节点」的时候都写一个递归。递归本身没什么问题,但每写一次就重复一遍遍历逻辑。而且不同的遍历顺序(前序、中序、后序、层序)混在业务代码里,维护成本直接翻倍。
java
public class TreeNode<T> implements Iterable<T> {
private T value;
private List<TreeNode<T>> children;
// 深度优先遍历
@Override
public Iterator<T> iterator() {
return new DepthFirstIterator<>(this);
}
// 广度优先遍历------换个迭代器就行
public Iterator<T> bfsIterator() {
return new BreadthFirstIterator<>(this);
}
}
业务代码从这样:
java
// 每次遍历树都要写一遍递归
void dfs(TreeNode<User> node, List<User> result) {
result.add(node.getValue());
for (TreeNode<User> child : node.getChildren()) {
dfs(child, result);
}
}
变成这样:
java
// 想怎么遍历就怎么遍历,算法和业务彻底解耦
for (User u : root) { // 深度优先
doSomething(u);
}
Iterator<User> it = root.bfsIterator();
while (it.hasNext()) {
doSomething(it.next()); // 广度优先
}
你上一次在项目里写 implements Iterable 是什么时候?如果从来没用过,那说明你可能一直在不同的文件里反复写同样的树遍历逻辑。
数据库的游标,就是迭代器模式在底层最暴力的应用
Java 里 ResultSet 的 next() 方法,就是迭代器模式。
java
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
String name = rs.getString("name");
}
为什么叫游标(Cursor)?因为数据库不会一次性把所有结果加载到内存。ResultSet 的默认行为是每次只从数据库取一行,取完再取下一行。你遍历到哪,游标指到哪。
这跟 ArrayList 的迭代器完全不同。ArrayList 的数据全在内存里,迭代器只是一个指针在移动。而 ResultSet 的迭代器背后可能是一个网络连接,每次 next() 都是一次网络传输。
所以如果有人告诉你「用迭代器遍历数据库结果集然后做复杂业务逻辑」,你应该警觉------游标长时间持有数据库连接,连接池分分钟被打满。
像 MyBatis 的 Cursor 接口就明确警告你:用完赶紧关。
java
try (Cursor<User> cursor = mapper.selectAllUsers()) {
for (User u : cursor) {
process(u);
}
} // 这里自动关闭 cursor,释放连接
一句话总结
迭代器模式不是 foreach 语法糖。它是遍历与数据结构的解耦,是 fail-fast 的设计契约,是流式处理的前置抽象,是数据库游标的底层思想。
你每天都在用它,但你从没想过它为什么长这样。下次写 foreach 的时候,脑子里过一遍 hasNext() 和 next() 在干什么------很多你以为玄学的问题,答案就在这两个方法里。
说起来,我最近在做一个用卡皮巴拉讲设计模式的小程序「爪爪代码冒险记」,用漫画加答题的方式把 23 个设计模式拆开讲。如果你觉得迭代器这类东西值得深入理解而不是「能用就行」,搜一下「爪爪代码冒险记」,应该能对上你的胃口。