foreach 语法糖背后,迭代器模式做了多少脏活

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 接口继承了 Iterableiterator() 方法返回一个 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.0Enumeration 接口,只有 hasMoreElements()nextElement(),不支持删除。Hashtable、Vector 用的就是这个。

Java 1.2 :引入 Iterator,加了 remove() 方法,Enumeration 被标记为过时。整个 Collections 框架基于迭代器模式重建。

Java 5 :引入 Iterable + foreach 语法糖,迭代器使用变得更优雅。

Java 8 :引入 Iterator.forEachRemaining()Spliterator,支持并行遍历。Stream 的底层就是基于 Spliterator 工作的。

每一次演进都在让迭代器更好用,但核心思想始终没变------把遍历行为从集合内部抽离出来

什么时候该自己实现迭代器

说实话,日常开发中你很少需要自己写一个全新的迭代器。Java 的集合框架已经覆盖了绝大多数场景。

但你会在这些情况下需要自定义迭代器:

  1. 自定义数据结构:比如树形结构的前序/中序/后序遍历、跳表遍历、B+ 树叶子节点遍历
  2. 特殊遍历逻辑:比如跳过某些元素、按优先级遍历、只遍历满足条件的元素
  3. 懒加载遍历:数据量大到不能一次性加载到内存,需要按需读取(类似数据库游标)

最后一个场景特别有意思。假设你要遍历一个 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 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章------每篇文章我都会同步对应的小程序内容。

相关推荐
HLAIA光子1 小时前
LLM缓存机制:你的API账单可以砍掉75%
后端·llm·ai编程
卷无止境1 小时前
统计质量控制(SQC / SPC):用数据说话的质量哲学
后端
XovH1 小时前
第 44篇 k8s之实战:将 Web 应用迁移到 Kubernetes(上)
后端
晓杰'1 小时前
从0到1实现Balatro游戏后端(7):Boss Blind与特殊规则实现
后端·websocket·typescript·node.js·游戏开发·项目实战·nestjs
MariaH1 小时前
Node.js 架构理解
后端
我登哥MVP1 小时前
Spring Boot 从“会用”到“精通”:请求映射原理
java·spring boot·后端·spring·servlet·maven·intellij-idea
MariaH1 小时前
Node-fs模块
后端
峰子20121 小时前
PG 管控系统技术方案
数据库·后端·pg
晓杰'2 小时前
从0到1实现Balatro游戏后端(6):Blind关卡状态设计与回合推进实现
后端·websocket·typescript·游戏开发·项目实战·nestjs·状态管理