写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面

写 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 里 ResultSetnext() 方法,就是迭代器模式。

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 个设计模式拆开讲。如果你觉得迭代器这类东西值得深入理解而不是「能用就行」,搜一下「爪爪代码冒险记」,应该能对上你的胃口。

相关推荐
LiaCode1 小时前
Redis 在生产项目的使用
前端·后端
用户559822481221 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode1 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战1 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha2 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn2 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425912 小时前
ShardingJDBC
后端
行者全栈架构师2 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
Colin草率地做慢慢地改2 小时前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构