设计模式-迭代器模式

发布于:2025-06-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

迭代器模式

一、核心思想(一句话概括)

迭代器模式提供了一种顺序访问一个聚合对象(如列表、集合)中各个元素的方法,而又无需暴露该对象的内部表示

简单来说,它的核心就是 “分离集合对象的遍历行为”。集合对象(Collection)只管存东西,而遍历(Traversal)的责任则交给迭代器(Iterator)。


二、一个生动的比喻:电视遥控器

想象一下你家里的各种设备:电视机、蓝光播放器、音响。

  • 聚合对象(Collection):就是这些设备本身(电视机、播放器等)。它们内部的构造非常复杂(电路板、芯片、显像管...)。

  • 遍历操作:就是“切换到下一个频道”或“播放下一首歌曲”。

如果没有迭代器模式: 你要换台,可能需要打开电视机后盖,找到调谐器,手动拨动一个旋钮。换歌,则需要操作蓝光播放器的激光头。你需要了解每种设备内部的实现细节才能操作它。这显然是荒谬的。

有了迭代器模式: 我们发明了遥控器(Iterator)。遥控器上有一个统一的接口,比如 “下一个(+)” 和 “上一个(-)” 按钮。

  • 你用电视遥控器的 “下一个(+)” 按钮,电视就切换到下一个频道。

  • 你用音响遥控器的 “下一个(+)” 按钮,音响就播放下一首歌曲。

你只需要学会使用遥控器(统一的遍历接口),而完全不需要关心电视机和音响的内部是如何实现“下一个”这个功能的。遥控器为你屏蔽了底层实现的复杂性

这就是迭代器模式的精髓:为遍历提供一个统一的接口,让客户端与集合的具体实现解耦。


三、要解决的问题

在软件开发中,我们有各种各样的数据集合:

  • ArrayList:底层是数组,通过索引 i++ 遍历最快。

  • LinkedList:底层是链表,通过 node = node.next 遍历最高效。如果用索引遍历会非常慢。

  • HashSet:底层是哈希表,根本没有索引的概念,遍历顺序也不确定。

如果我们的业务代码直接依赖这些集合的具体实现来写遍历循环,会怎么样?

// 客户端代码
public void printElements(ArrayList<String> list) {
    for (int i = 0; i < list.size(); i++) { // 依赖 ArrayList 的索引
        System.out.println(list.get(i));
    }
}
​
public void printElements(LinkedList<String> list) {
    // 糟糕,如果换成 LinkedList,上面的代码效率极低
    // 我必须为 LinkedList 重写一个版本
}

这会导致:

  1. 暴露内部细节:客户端代码被迫知道了 ArrayList 是基于索引的。

  2. 代码缺乏通用性:如果把 ArrayList 换成 LinkedList 或 HashSet,遍历代码就要重写。这违反了“开闭原则”。

迭代器模式就是来解决这个问题的。


四、迭代器模式的结构与实现

它主要包含四个角色:

  1. Iterator(迭代器接口):定义了遍历所需的基本方法,最核心的是:

    • hasNext(): 判断是否还有下一个元素。

    • next(): 返回下一个元素,并将指针后移。

    • remove(): (可选) 删除当前元素。

  2. ConcreteIterator(具体迭代器):实现 Iterator 接口,负责对特定的聚合对象进行遍历。它内部需要维持一个遍历的状态(比如当前索引、当前节点引用)。

  3. Aggregate(聚合接口):定义了集合应该具备的方法,其中最核心的是 iterator(),它负责返回一个 Iterator 对象。

  4. ConcreteAggregate(具体聚合):实现 Aggregate 接口,是具体的集合类。它实现了 iterator() 方法,返回一个与自己相匹配的 ConcreteIterator 实例。

代码实现(以一个自定义的“书架”为例)

第1步:定义 Iterator 和 Aggregate 接口

// Iterator 接口
public interface Iterator<E> {
    boolean hasNext();
    E next();
}
​
// Aggregate 接口
public interface Aggregate<E> {
    Iterator<E> iterator();
}

第2步:创建具体聚合类 (ConcreteAggregate)

// 具体的集合:书架
public class BookShelf implements Aggregate<Book> {
    private Book[] books;
    private int last = 0; // 当前书的数量
​
    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }
​
    public Book getBookAt(int index) {
        return books[index];
    }
​
    public void addBook(Book book) {
        if (last < books.length) {
            this.books[last] = book;
            last++;
        }
    }
​
    public int getLength() {
        return last;
    }
​
    // 核心:返回一个为 BookShelf 服务的迭代器实例
    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }
}

第3步:创建具体迭代器类 (ConcreteIterator)

// 为 BookShelf 服务的具体迭代器
public class BookShelfIterator implements Iterator<Book> {
    private BookShelf bookShelf;
    private int index;
​
    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }
​
    @Override
    public boolean hasNext() {
        // 判断索引是否超出了书架上书的总数
        return index < bookShelf.getLength();
    }
​
    @Override
    public Book next() {
        if (!hasNext()) {
            throw new java.util.NoSuchElementException();
        }
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }
}
​
// 书的实体类(略)
class Book { private String name; public Book(String name) {this.name = name;} public String getName() {return name;} }

第4步:客户端使用

public class Client {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.addBook(new Book("设计模式"));
        bookShelf.addBook(new Book("Java编程思想"));
        bookShelf.addBook(new Book("代码整洁之道"));
        bookShelf.addBook(new Book("深入理解Java虚拟机"));
​
        // 客户端通过统一的 Iterator 接口来遍历,完全不关心 BookShelf 内部是数组还是别的
        Iterator<Book> it = bookShelf.iterator();
        while (it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }
    }
}

看,客户端代码多么干净!它只依赖于 Iterator 接口,完全不关心 BookShelf 是怎么存书的。


五、Java 中的迭代器模式

你其实每天都在使用它!Java 的集合框架(JCF)就是迭代器模式的完美实践。

  • java.util.Iterator 就是我们的 Iterator 接口。

  • java.lang.Iterable 就是我们的 Aggregate 接口。它只有一个方法 iterator()。

  • 所有 Java 集合类(ArrayList, LinkedList, HashSet等)都实现了 Iterable 接口,它们就是 ConcreteAggregate

  • 每个集合类都有一个内部类来实现 Iterator 接口,比如 ArrayList 有 Itr,LinkedList 有 ListItr,它们就是 ConcreteIterator

这就是为什么我们可以对所有 Java 集合使用 for-each 循环

List<String> list = new ArrayList<>();
// ... add elements
​
// for-each 循环是迭代器模式的语法糖!
for (String item : list) { 
    System.out.println(item);
}

编译器会自动将上面的 for-each 循环转换成下面这样基于迭代器的代码:

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    System.out.println(item);
}

六、优缺点

优点
  1. 封装性好:将遍历逻辑从集合中分离出来,客户端无需知道集合的内部结构。

  2. 单一职责原则:集合负责存储,迭代器负责遍历,职责清晰。

  3. 通用性强:为所有集合提供了统一的遍历接口,简化了客户端代码。

  4. 支持多种遍历:可以对同一个集合创建多个迭代器,每个迭代器都独立地维护自己的遍历状态。

缺点
1. 结构复杂性与类的增加

这是我之前提到的那点,但可以更深入一些。对于每一种聚合(集合)类型,理论上都需要一个与之对应的具体迭代器类。

  • 简单场景下的过度设计:如果你的系统非常简单,只有一个 ArrayList,而且未来也不太可能改变,那么专门为其设计接口和迭代器类,确实会比直接用 for (int i=0; ...) 循环显得“重”。

  • 平行的类层次结构:你需要在系统中维护两个平行的类层次结构:一个是聚合类的层次结构(List, Set 等),另一个是迭代器类的层次结构(ArrayListIterator, HashSetIterator 等)。这增加了系统的整体复杂性。

2. 并发修改的限制(非常重要!)

这是迭代器模式在实际应用中最常遇到的一个“坑”,尤其是在多线程环境下。

Java 集合框架中的绝大多数迭代器都是快速失败(Fail-Fast)的。

  • 什么是“快速失败”? 当你在使用一个迭代器遍历集合时,如果有另一个线程(或者甚至是当前线程通过集合本身的方法而不是迭代器的方法)修改了集合的结构(比如添加、删除元素),那么这个迭代器会立刻抛出 ConcurrentModificationException 异常。

  • 为什么会这样? ArrayList 等集合内部有一个计数器 modCount。每当集合的结构被修改(调用 add, remove 等方法),modCount 就会加一。 当你创建迭代器时,迭代器会记下当时的 modCount 值。在每次调用迭代器的 next() 方法时,它都会检查自己记录的 modCount 是否和集合当前的 modCount 一致。如果不一致,就说明集合在遍历期间被外部修改了,迭代器会立即“失败”(抛出异常),以避免在不确定的状态下继续遍历,从而产生无法预料的后果。

  • 带来的缺点: 这使得标准的迭代器在多线程共享集合的场景下几乎无法直接使用。你必须采取额外的同步措施(比如使用 synchronized 锁住整个集合),或者使用专门为并发设计的集合类(如 ConcurrentHashMap 或 CopyOnWriteArrayList),它们的迭代器是弱一致性快照式的,不会抛出此异常。

3. 遍历方式的局限性

标准的 java.util.Iterator 接口功能非常基础,它只支持单向的、向前的遍历

  • 无法后退:你不能通过 Iterator 接口从后往前遍历,或者在元素之间来回移动。

  • 需要专门的子接口:为了解决这个问题,Java 提供了 ListIterator 接口(专用于 List),它继承了 Iterator 并增加了 hasPrevious(), previous(), add() 等方法,允许双向遍历和在遍历时修改列表。这说明基础的迭代器模式本身功能有限,需要扩展来满足更复杂的需求。

4. remove() 方法的复杂性与可选性

Iterator 接口中的 remove() 方法给使用者带来了一些心智负担。

  • 可选实现:remove() 是一个“可选操作”。这意味着一个迭代器完全可以不支持删除功能。如果调用一个不支持删除的迭代器的 remove() 方法,它会抛出 UnsupportedOperationException。这就要求使用者在使用前必须清楚该迭代器是否支持此操作。

  • 状态依赖:remove() 的调用是有状态的。它必须在调用 next() 之后、并且在下一次调用 next() 之前被调用。如果你连续调用两次 remove(),或者在没有调用 next() 的情况下就调用 remove(),都会抛出 IllegalStateException。这种严格的调用顺序要求开发者必须小心翼翼。

5. 轻微的性能开销

在对性能要求极其苛刻的场景下,迭代器模式会引入一些微小的开销。

  • 对象创建:每次调用 iterator() 都会创建一个新的迭代器对象,这有微小的内存和CPU开销。

  • 方法调用:通过 hasNext() 和 next() 进行的调用是方法调用,相比于直接在 for 循环中通过索引访问数组元素(array[i]),开销会略大一些。

但是,必须强调:对于 99.9% 的应用程序来说,这点性能差异完全可以忽略不计。通过迭代器模式换来的代码解耦、可维护性和安全性的收益,远远超过这点微不足道的性能损失。过早地为此进行优化是典型的大忌。