【基础数据篇】数据遍历大师:Iterator模式

1. 前言

在日常编程中,我们几乎无时无刻不在与"集合"打交道:数组、列表、哈希表、树形结构...这些集合承载着我们的数据。而处理数据最常见的操作之一,就是遍历------逐个访问集合中的每个元素。

试想一下,如果没有一种统一的遍历方式,我们会面临什么?对于数组,我们得用 for (int i = 0; i < array.length; i++);对于链表,我们可能需要 while (node != null);而对于更复杂的树或图,遍历逻辑就更加复杂多样了。当业务代码需要处理不同类型的集合时,就必须了解它们各自的内置结构,这导致了代码的高度耦合和复杂性。

Iterator(迭代器)模式正是为了解决这个问题而生的。它旨在提供一种统一的方法来顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。简单来说,Iterator 模式将遍历的逻辑从集合本身中分离出来,交给一个名为"迭代器"的对象负责。

2. 定义

Iterator 模式是一种行为设计模式,它定义了一个接口,用于顺序访问集合对象中的元素,而无需了解其底层的实现细节。该模式通常涉及两个核心角色:

  1. 迭代器接口: 定义了遍历集合所需的基本操作,通常包含:
    • next(): 返回集合中的下一个元素。
    • hasNext(): 判断是否还有更多元素可以遍历。
    • (可选)remove(): 从集合中移除当前元素。
  2. 可迭代集合: 提供一个方法(通常称为 iterator())用于获取一个指向集合开头的迭代器对象。
text 复制代码
迭代器模式的核心思想是:将对聚合对象的访问和遍历职责分离,从而允许聚合专注于数据存储,而迭代器专注于遍历算法。这体现了单一职责原则,并通过抽象接口化解了聚合与遍历操作之间的静态耦合。

3. 应用

Iterator 模式的应用场景非常广泛,几乎在所有现代编程语言的标准库中都有它的身影。

3.1 场景一:统一遍历不同集合

这是最经典的场景,假设有一个方法需要打印所有元素,无论传入的是 ArrayList 还是 HashSet,你都可以使用相同的 Iterator 接口。

java 复制代码
// 定义一个打印方法
public void printAll(Iterator<String> iterator) {
    while (iterator.hasNext()) {
        String element = iterator.next();
        System.out.println(element);
    }
}

// 使用
List<String> list = Arrays.asList("A", "B", "C");
Set<String> set = new HashSet<>(Arrays.asList("X", "Y", "Z"));

printAll(list.iterator()); // 遍历列表
printAll(set.iterator());  // 遍历集合,代码无需任何改动!

3.2 场景二:简化语法(增强型for循环)

Java 的 for-each 循环和 Python 的 for ... in 循环,其底层实现就是 Iterator 模式。编译器会自动将这种语法糖转换为使用迭代器的代码。

java 复制代码
// 语法糖(开发者这样写)
for (String item : list) {
    System.out.println(item);
}

// 编译器处理后(实际执行)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    System.out.println(item);
}

3.3 场景三:支持多种遍历方式

一个复杂的集合(如一棵树)可以同时提供多种迭代器,例如"前序遍历迭代器"、"中序遍历迭代器"和"广度优先遍历迭代器"。客户端可以根据需要选择不同的迭代器,而不需要修改集合类本身。

假设我们有一个简单的二叉树结构:

java 复制代码
// 二叉树节点
class TreeNode {
    String value;
    TreeNode left;
    TreeNode right;
    TreeNode(String value) { this.value = value; }
}

// 二叉树集合类
class BinaryTree {
    TreeNode root;
    // ...

    // 提供不同的迭代器
    public Iterator<String> getPreOrderIterator() {
        return new PreOrderIterator(root);
    }
    public Iterator<String> getInOrderIterator() {
        return new InOrderIterator(root);
    }
    public Iterator<String> getLevelOrderIterator() {
        return new LevelOrderIterator(root);
    }
}

下面给出前序遍历迭代器的参考实现:

java 复制代码
class PreOrderIterator implements Iterator<String> {
    private Stack<TreeNode> stack = new Stack<>();

    public PreOrderIterator(TreeNode root) {
        if (root != null) stack.push(root);
    }

    @Override
    public boolean hasNext() {
        return !stack.isEmpty();
    }

    @Override
    public String next() {
        if (!hasNext()) throw new NoSuchElementException();
        TreeNode node = stack.pop();
        // 注意入栈顺序:先右后左,保证出栈时是左先于右
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
        return node.value;
    }
}

3.4 场景四:延迟遍历或无限集合

迭代器的"按需计算"(或称"懒加载")特性使其在处理海量数据流或理论上无限的序列时极具价值。它允许我们创建一个逻辑上的集合,而无需在内存中实现存储所有元素。

示例1:海量数据分页遍历 假设需要从数据库或网络中分批读取大量数据,迭代器可以隐藏分页获取的细节。

java 复制代码
// 一个模拟从数据库分页读取的迭代器
class PaginatedDataIterator implements Iterator<String> {
    private int currentPage = 0;
    private int pageSize = 100;
    private Iterator<String> currentPageData;

    private List<String> fetchPageFromDatabase(int page) {
        // 模拟数据库查询:SELECT ... LIMIT pageSize OFFSET page*pageSize
        // 这里返回模拟数据
        List<String> data = new ArrayList<>();
        for (int i = 0; i < pageSize; i++) {
            data.add("Item-" + (page * pageSize + i));
        }
        return data;
    }

    @Override
    public boolean hasNext() {
        // 如果当前页数据为空或已遍历完,则尝试获取下一页
        if (currentPageData == null || !currentPageData.hasNext()) {
            List<String> newPage = fetchPageFromDatabase(currentPage);
            if (newPage.isEmpty()) {
                return false; // 没有更多数据了
            }
            currentPageData = newPage.iterator();
            currentPage++;
        }
        return true;
    }

    @Override
    public String next() {
        if (!hasNext()) throw new NoSuchElementException();
        return currentPageData.next();
    }
}

// 使用:客户端可以像遍历普通集合一样处理海量数据
Iterator<String> massiveDataIterator = new PaginatedDataIterator();
while (massiveDataIterator.hasNext()) {
    String item = massiveDataIterator.next();
    process(item); // 每次只在内存中保留一小批数据
    if (someCondition) break; // 可以随时中断,避免不必要的加载
}

示例2:无限序列生成器 迭代器可以代表一个永不结束的序列,元素只在被请求时才生成。

java 复制代码
// 生成无限斐波那契数列的迭代器
class FibonacciIterator implements Iterator<BigInteger> {
    private BigInteger a = BigInteger.ZERO;
    private BigInteger b = BigInteger.ONE;

    @Override
    public boolean hasNext() {
        return true; // 总是有下一个元素,因为序列是无限的
    }

    @Override
    public BigInteger next() {
        BigInteger nextVal = a;
        // 更新状态为下一个数
        a = b;
        b = nextVal.add(b);
        return nextVal;
    }
}

// 使用:可以按需获取序列中的值,而不必(也无法)预先计算所有值
Iterator<BigInteger> fibIterator = new FibonacciIterator();
for (int i = 0; i < 100; i++) { // 只取前100个,尽管序列是无限的
    System.out.println(fibIterator.next());
}

// 或者与Stream等惰性求值机制结合使用(在Java中)
// Stream.generate(() -> fibIterator.next()).limit(10).forEach(...);

4. 开源代码解析

4.1 顶层设计:IteratorIterable 接口

  • 分离关注点Iterable 表示"可迭代的",职责是提供一个迭代器。Iterator 表示"迭代器",职责是控制遍历过程。这种分离使得一个集合可以同时提供多种遍历方式(如 TreeSet 可以提供升序和降序迭代器)。
java 复制代码
// java.util.Iterator
public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    // JDK 8+ 新增:对剩余元素执行操作,常用于函数式编程
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

// java.lang.Iterable
public interface Iterable<T> {
    Iterator<T> iterator();
    // JDK 8+ 新增的默认方法,支持forEach循环
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    // 并行流相关,暂不深入
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

4.2 ArrayList.Itr:快速失败(Fail-Fast)机制的典范

ArrayList 的迭代器是理解 Java 迭代器核心机制的最佳起点。它是一个私有内部类,从而可以无缝访问外部类的所有成员。

java 复制代码
// 在 ArrayList 类内部
private class Itr implements Iterator<E> {
    int cursor;       // 下一个要返回元素的索引,初始为0
    int lastRet = -1; // 最后一个返回元素的索引;如果没有元素则返回-1(用于remove操作)
    int expectedModCount = modCount; // 关键!初始化时记录外部ArrayList的修改次数

    Itr() {} // 构造函数

    public boolean hasNext() {
        return cursor != size; // 简单比较游标和集合大小
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification(); // 【关键点1】检查是否发生并发修改
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData; // 获取外部类的数组引用
        if (i >= elementData.length)
            throw new ConcurrentModificationException(); // 【关键点2】另一种检查
        cursor = i + 1;
        return (E) elementData[lastRet = i]; // 返回元素,并记录lastRet
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification(); // 检查并发修改

        try {
            ArrayList.this.remove(lastRet); // 调用外部类的remove方法
            cursor = lastRet; // 游标回退,因为后面的元素都前移了一位
            lastRet = -1; // 重置lastRet,防止连续remove
            expectedModCount = modCount; // 【关键点3】remove后,更新expectedModCount!
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    // 核心检查方法:快速失败(Fail-Fast)的灵魂
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

深度解析:

  1. 快速失败(Fail-Fast)机制
    • modCountArrayList 的一个成员变量,任何会结构性修改 集合的操作(如 add, remove, clear)都会使 modCount++
    • 迭代器在创建时捕获当前的 modCount 值,存为 expectedModCount
    • 在每次迭代操作(next, remove)前,都会检查 modCount != expectedModCount。如果不相等,说明在迭代过程中集合被外部修改了,立即抛出 ConcurrentModificationException
    • 设计意图:尽早发现并发修改的bug,避免出现不确定的行为(比如无限循环、跳过元素、重复元素)。
  2. remove 方法的特殊处理
    • 迭代器自己的 remove 方法是唯一"合法"的在迭代中修改集合的方式。
    • 在成功调用 ArrayList.this.remove(lastRet) 后,它会主动将 expectedModCount 更新为新的 modCount。这样,迭代器"认可"这次修改,不会抛出异常。
    • 这解释了为什么用 for-each 循环(底层使用迭代器)时直接调用 list.remove(item) 会抛异常,而使用 iterator.remove() 则不会。

4.3 HashMap 的迭代器:应对散列冲突的遍历

HashMap 的迭代更复杂,因为它需要遍历"数组 + 链表/红黑树"。其迭代器是 HashIterator 抽象类,KeyIterator, ValueIterator, EntryIterator 都是它的简单派生类。

java 复制代码
// 在 HashMap 类内部
abstract class HashIterator {
    Node<K,V> next;        // 下一个要返回的条目
    Node<K,V> current;     // 当前条目
    int expectedModCount;  // 用于快速失败
    int index;             // 当前所在的哈希桶索引

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        // 初始化:找到第一个非空的桶(bucket)
        if (t != null && size > 0) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        // 关键:如何找到下一个元素
        if ((next = (current = e).next) == null && (t = table) != null) {
            // 如果当前链表/树下没有下一个节点,则寻找下一个非空的桶
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(key, null, false, true); // 调用HashMap的移除方法
        expectedModCount = modCount; // 更新
    }
}

深度解析:

  1. 遍历顺序与不确定性
    • HashMap 迭代器的遍历顺序是桶(bucket)的顺序 ,从 table[0]table[table.length-1]。对于每个桶,再遍历其中的链表或红黑树。
    • 这个顺序不是 插入顺序,也不是 键的排序顺序。它取决于哈希值、桶的大小和冲突解决策略。因此,不要依赖 HashMap 的迭代顺序
  2. 性能考量
    • 迭代性能与 HashMapsize(元素数量)和 capacity(桶数组大小)有关。一个具有较大容量但元素很少的 HashMap(负载因子低),迭代起来可能比元素数量相同的 ArrayList 慢,因为它需要跳过很多空桶。
    • LinkedHashMap 通过维护一个贯穿所有条目的双向链表,提供了可预测的迭代顺序(通常是插入顺序或访问顺序),解决了这个问题,但牺牲了一些空间和插入/删除性能。

4.4 并发场景下的迭代器:CopyOnWriteArrayList

ArrayList 的快速失败机制并不保证真正的线程安全,它只是"尽力而为"地抛出异常来提醒你。真正的并发安全集合,比如 CopyOnWriteArrayList,采用了完全不同的策略。

java 复制代码
// CopyOnWriteArrayList 的迭代器
public Iterator<E> iterator() {
    // 直接返回底层数组当前快照(snapshot)的迭代器!
    return new COWIterator<E>(getArray(), 0);
}

static final class COWIterator<E> implements ListIterator<E> {
    // 迭代器持有创建时数组的快照引用
    private final Object[] snapshot;
    private int cursor;

    COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements; // 注意:这里只是引用赋值,但外部数组是volatile且写时复制
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public E next() {
        if (!hasNext()) throw new NoSuchElementException();
        // 所有操作都在快照数组上进行,这个数组在迭代器生命周期内永远不会变
        return (E) snapshot[cursor++];
    }
    // ... 其他方法,remove/set/add 通常不支持,直接抛异常
}

深度解析:

  • 原理 :当需要修改集合时(如 add, set),不直接在原数组上操作,而是将原数组复制一份,在副本上进行修改,修改完成后,再将集合的引用指向新的数组。
  • 迭代器行为 :迭代器持有的是创建那一刻的数组快照。在迭代器生命周期内,无论其他线程如何修改集合(增删改),迭代器遍历的都是那个"旧的"快照。
  • 优势迭代操作永远不需要加锁,永远不会抛出 ConcurrentModificationException,性能极高。
  • 代价
    1. 内存开销:写操作会导致数组复制,内存占用大。
    2. 数据延迟 :迭代器看不到迭代开始后发生的修改,这种特性称为弱一致性。这可能是优点也可能是缺点,取决于业务场景。

5. 总结

迭代器模式不仅仅是为了"方便遍历"。其核心是一种解耦思想,属于行为型设计模式。

  1. 职责分离
    • 聚合对象(集合) :核心职责是高效地存储和管理数据。它不应该关心"如何遍历"这些数据。
    • 迭代器对象 :核心职责是定义和执行遍历算法。它封装了遍历所需的游标、顺序等状态和逻辑。
    • 价值 :这使得集合和遍历算法可以独立地变化和扩展。你可以轻松地为同一个集合增加多种遍历方式(如正序、逆序、广度优先),而无需修改集合本身的代码,符合开闭原则
  2. 抽象接口
    • 模式通过统一的 Iterator 接口,将遍历这个行为抽象出来。客户端代码只依赖于这个抽象接口,而不是具体的集合实现,从而降低了耦合度
  3. 封装复杂性
    • 对于复杂数据结构(如树、图),遍历逻辑可能非常复杂。迭代器将这些复杂性隐藏在内部,为客户端提供一个简单的 next()hasNext() 接口,简化了客户端的使用

Java 根据集合的不同特性,实现了多种迭代器策略,体现了权衡的艺术

集合类型 迭代器策略 特点 适用场景
ArrayList, HashMap 快速失败 性能好,直接访问底层数组/链表。严禁在迭代中被外部修改。 单线程环境,或能确保迭代期间无修改的场景。
CopyOnWriteArrayList 弱一致性 基于创建时的数据快照 进行迭代。迭代过程中集合的修改对迭代器不可见。永不抛 ConcurrentModificationException 读多写极少的高并发场景。牺牲写性能(复制数组)和内存,换取读的高性能和无锁遍历。
LinkedHashMap 顺序迭代 通过维护一个双向链表,提供了可预测的迭代顺序(插入顺序或访问顺序)。 需要按特定顺序遍历 Map 的场景。
ConcurrentHashMap 弱一致性 迭代器反映的是创建时或之后某个点的状态。可以容忍并发修改,不会抛出异常,但不保证能迭代到所有创建后新加入的元素。 高并发读写场景。在安全性和一致性之间取得平衡。

在下一篇《【基础数据篇】数据访问守卫:Accessor模式》中,我们将探讨如何优雅地控制对对象属性的读写,封装数据访问逻辑,保障数据安全。

相关推荐
用户4099322502123 小时前
想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷?
后端·ai编程·trae
muxin-始终如一3 小时前
系统重构过程以及具体方法
设计模式·重构
xuejianxinokok3 小时前
什么是代数类型 ? java为什么要添加record,Sealed class 和增强switch ?
后端·rust
洛小豆3 小时前
Git打标签仓库看不到?她说:豆子,你又忘了加 --tags!
git·后端·github
LawsonJin4 小时前
springboot实现微信小程序支付(服务商和普通商户模式)
spring boot·后端·微信小程序
福大大架构师每日一题4 小时前
2025-10-16:有向无环图中合法拓扑排序的最大利润。用go语言,给定一个由 n 个节点(编号 0 到 n-1)构成的有向无环图,边集合用二维数组 edge
后端
只玩代码4 小时前
技术拆解:基于 Rokid CXR-M SDK 构建“AI 实时翻译眼镜伴侣”核心逻辑
后端
码码宇4 小时前
技术拆解:Rokid CXR-M SDK 如何构建流畅AR演讲提词功能
后端
沐眼4 小时前
技术拆解:Rokid CXR-M SDK 构建 AI 智能提词眼镜助手连接到场景落地
后端