GoF设计模式——迭代器模式
为什么需要迭代器模式假设需要写一个打印集合所有元素的方法业务里存在三种集合数组、自定义链表、HashMap。没有统一的遍历接口时客户端只能这么写// 遍历数组用下标public void printArray(String[] arr) {for (int i 0; i arr.length; i) {System.out.println(arr[i]);}}// 遍历自定义链表用 next 指针public void printLinkedList(Node head) {Node cur head;while (cur ! null) {System.out.println(cur.value);cur cur.next;}}// 遍历 HashMap用 entrySetpublic void printMap(MapString, Integer map) {for (Map.EntryString, Integer e : map.entrySet()) {System.out.println(e.getKey() “” e.getValue());}}问题一目了然客户端被迫知道每种集合的内部结构——数组要靠下标、链表要靠 next 指针、HashMap 要靠 entrySet。三种集合就要写三份代码。更严重的是一旦集合的内部结构变了比如把数组换成红黑树所有调用点都得改。问题症结在于把遍历和数据结构绑死了忽略了 “下一个元素在哪是集合自己的事客户端只关心有没有下一个和给我下一个”。迭代器模式解决的就是这个问题——把游走的动作从数据结构里剥出来统一成一个接口客户端从此不再关心底层是数组、链表还是树。概念迭代器模式Iterator Pattern是一种行为型设计模式核心思想是提供一种统一的方式来顺序访问一个聚合对象中的各个元素而不暴露该对象的内部表示。可以把它想象成博物馆的语音导览器无论展品是按年代排、按国别排还是按主题排游客只需要按下一件按钮就能听到下一个展品的介绍——完全不需要知道展品是怎么被组织的。展馆负责摆展内部结构导览器负责引路遍历接口游客只需要跟着走。迭代器模式涉及四个角色Iterator迭代器接口定义遍历元素的接口通常包含 hasNext() 判断是否还有下一个元素、next() 返回当前元素并将游标后移ConcreteIterator具体迭代器实现迭代器接口维护当前遍历位置实现具体的遍历逻辑Aggregate聚合接口定义创建迭代器的接口通常包含一个 createIterator() 方法ConcreteAggregate具体聚合类实现聚合接口负责存储数据并返回与自身匹配的具体迭代器实现实现创建返回«interface»AggregatecreateIterator() : IteratorConcreteAggregate-elementscreateIterator() : Iterator«interface»IteratorhasNext() : booleannext() : ObjectConcreteIterator-index: inthasNext() : booleannext() : Object图中各类之间的关系Aggregate 接口声明 createIterator 方法ConcreteAggregate 实现它——持有元素数据并创建能遍历自身元素的 ConcreteIterator。Iterator 接口声明 hasNext 和 nextConcreteIterator 用一个 index 字段维护遍历状态。客户端只依赖 Aggregate 和 Iterator 两个接口完全不接触任何具体类。实现迭代器模式的核心是游标外置——把当前遍历到哪儿这个状态从聚合对象里拿出来放到一个专门的迭代器对象里。这样同一个集合可以同时被多个迭代器遍历互不干扰。基础实现定义 Iterator 接口声明 hasNext 和 nextConcreteIterator 用一个 index 字段追踪当前位置。定义 Aggregate 接口声明 createIteratorConcreteAggregate 内部用数组存数据并创建对应迭代器。客户端拿到迭代器后用 while (it.hasNext()) 循环遍历。// Iterator迭代器接口interface Iterator {boolean hasNext();Object next();}// ConcreteIterator具体迭代器class ConcreteIterator implements Iterator {private Object[] elements;private int index; // 游标记录当前位置public ConcreteIterator(Object[] elements) { this.elements elements; this.index 0; } public boolean hasNext() { return index elements.length; } public Object next() { if (!hasNext()) { return null; } // 后置自增先返回当前元素再把游标后移 return elements[index]; }}// Aggregate聚合接口interface Aggregate {Iterator createIterator();}// ConcreteAggregate具体聚合类class ConcreteAggregate implements Aggregate {private Object[] elements;public ConcreteAggregate(Object[] elements) { this.elements elements; } public Iterator createIterator() { return new ConcreteIterator(elements); }}// 客户端class Client {public static void main(String[] args) {String[] data {“hello”, “world”, “java”};Aggregate aggregate new ConcreteAggregate(data);Iterator it aggregate.createIterator();while (it.hasNext()) {System.out.println(it.next());}}}角色对照Iterator迭代器接口Iterator声明 hasNext 和 nextConcreteIterator具体迭代器ConcreteIterator用 index 追踪位置Aggregate聚合接口Aggregate声明 createIteratorConcreteAggregate具体聚合类ConcreteAggregate创建与自身匹配的迭代器关键点next 中的 index 是后置自增——先返回 elements[index] 再让 index index 1语义上正好对应返回当前元素并将游标后移。客户端只依赖 Aggregate 和 Iterator 两个接口替换集合实现数组→链表→跳表时客户端代码一行不改。同一个 ConcreteAggregate 可以调用 createIterator 多次得到多个独立的迭代器互不影响——这就是游标外置的价值。引入一个具体场景订单系统需要对外提供订单遍历能力但不希望暴露内部存储用的是数组未来可能换成 List 或数据库。让 OrderCollection 实现 Iterable就能直接支持增强 for 循环。// Element订单class Order {private String orderId;private double amount;public Order(String orderId, double amount) { this.orderId orderId; this.amount amount; } public String getOrderId() { return orderId; } public double getAmount() { return amount; } Override public String toString() { return Order{ orderId , ¥ amount }; }}// ConcreteAggregate订单集合实现 Iterable 让 for-each 直接可用class OrderCollection implements Iterable {private Order[] orders;private int size;public OrderCollection(int capacity) { this.orders new Order[capacity]; this.size 0; } public void addOrder(Order order) { if (size orders.length) { orders[size] order; } } // 创建迭代器返回内部类实例 public java.util.IteratorOrder iterator() { return new OrderIterator(); } // ConcreteIterator内部类迭代器 private class OrderIterator implements java.util.IteratorOrder { private int index 0; public boolean hasNext() { return index size; } public Order next() { return orders[index]; } }}// 使用class OrderDemo {public static void main(String[] args) {OrderCollection orders new OrderCollection(10);orders.addOrder(new Order(“ORD-001”, 299.0));orders.addOrder(new Order(“ORD-002”, 599.0));orders.addOrder(new Order(“ORD-003”, 149.5));// 客户端不知道内部是数组只知道能 for-each for (Order order : orders) { System.out.println(order); } }}角色对照Iterator迭代器接口java.util.IteratorJDK 已提供ConcreteIterator具体迭代器OrderIterator作为 OrderCollection 的内部类Aggregate聚合接口java.lang.IterableJDK 已提供ConcreteAggregate具体聚合类OrderCollection关键点把迭代器写成内部类能直接访问外部类的 orders 和 size 字段——这是 Java 内部类的特权比外部实现的迭代器少一次数据传递。实现 Iterable 接口后自动支持 for-each 语法编译器会把 for (Order o : orders) 展开成基于 iterator() 的循环。客户端看到的是能 for-each 的东西完全不知道内部是数组。总结本质把如何遍历从聚合对象中分离出来用统一的接口对外提供顺序访问能力。什么时候用需要向外暴露集合的元素但不想暴露内部结构数组、链表、树、跳表都能用同一套接口需要为同一个集合提供多种遍历方式前序/中序/后序、BFS/DFS需要同时进行多个独立遍历多个游标互不干扰想让客户端能用 for-each 语法遍历自定义集合什么时候不用集合就是内部使用客户端不需要遍历只需要一次简单遍历直接用 for-each 数组/List 更清爽性能极度敏感的场景——迭代器的方法调用比下标访问慢一点点集合结构会在遍历过程中频繁修改且不需要 fail-fast——这时候要单独设计并发遍历策略简单记忆内部结构不外露游标外置数下一步集合导航靠接口for-each 只需实现 Iterable。相似模式区分总览模式 核心意图 典型场景迭代器 提供统一的遍历接口 集合遍历、for-each、ResultSet 封装组合 将对象组合成树形结构统一处理叶子和容器 菜单树、文件系统、组织架构访问者 不修改元素类的前提下定义新操作 语法树处理、多态操作分派简单记忆组合管怎么组织迭代器管怎么走过每个元素访问者管到了每个元素之后做什么。迭代器 vs 组合两者都常见于处理集合的场景但关注点截然不同维度 迭代器模式 组合模式核心意图 顺序访问集合元素隐藏遍历细节 将对象组合成树形结构统一处理单元和组合结构差异 Iterator Aggregate 两个接口 Component 抽象接口 Leaf Composite关注点 怎么走过每个元素 怎么组织元素成层次结构典型场景 遍历 List、Map、Set 菜单树、文件系统、GUI 控件树逐步区分法如果需要逐个访问集合中的元素 → 选迭代器如果需要把对象组织成树形层次结构 → 选组合如果既有树形结构又要遍历 → 两者结合组合建结构迭代器遍历节点简单记忆口诀组合管怎么组织迭代器管怎么遍历。推荐树形菜单、文件目录这类场景通常两者一起用——用组合模式构建树同时提供迭代器把树拉平成线性序列客户端可以用 for-each 直接遍历整棵树。迭代器 vs 访问者两者都涉及依次处理集合元素但一个管路径、一个管动作维度 迭代器模式 访问者模式核心意图 提供遍历路径怎么走到每个元素 提供处理操作到每个元素做什么结构差异 Iterator Aggregate Visitor Element双重分派关注点 遍历过程 操作逻辑扩展方向 增加新集合类型 → 加迭代器 增加新操作 → 加访问者典型场景 集合元素的顺序访问 抽象语法树、编译器、复杂对象结构逐步区分法如果只需要逐个拿到元素做简单处理 → 选迭代器如果需要在不修改元素类的前提下定义多种新操作 → 选访问者如果元素类型多样且不同类型要执行不同逻辑 → 选访问者利用双重分派简单记忆口诀迭代器管怎么走到每个元素访问者管到了之后做什么。推荐单纯遍历用迭代器就够了只有当多种类型 × 多种操作的矩阵扩展成为瓶颈时才需要访问者。90% 的业务场景迭代器足以。练习题目学生名单题目描述小明是一位老师在进行班级点名时希望有一个学生名单系统请实现迭代器模式提供一个迭代器使得可以按顺序遍历学生列表。输入描述第一行是一个整数 N1 N 100表示学生的数量。接下来的 N 行每行包含一个学生的信息格式为 姓名 学号。输出描述输出班级点名的结果即按顺序遍历学生列表输出学生的姓名和学号。输入示例3Alice 1001Bob 1002Charlie 1003输出示例Alice 1001Bob 1002Charlie 1003解题思路题目要求实现标准的迭代器模式——遍历路径由迭代器负责客户端只调用 hasNext 和 next。角色对应Iterator迭代器接口MyIterator声明 hasNext 和 nextConcreteIterator具体迭代器StudentIterator用 index 追踪位置Aggregate聚合接口MyIterable声明 createIteratorConcreteAggregate具体聚合类StudentSystem持有学生数组核心逻辑输入完 N 个学生后构造 StudentSystem通过 createIterator 拿到迭代器迭代器内部用 index 后置自增——先返回当前学生再后移游标客户端用 while (it.hasNext()) 循环完全不接触底层数组import java.util.*;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int n sc.nextInt();Student[] students new Student[n];for (int i 0; i n; i) {String name sc.next();String number sc.next();students[i] new Student(name, number);}StudentSystem ss new StudentSystem(students);MyIterator it ss.createIterator();while (it.hasNext()) {Student s it.next();s.show();}}}// Iterator迭代器接口interface MyIterator {boolean hasNext();Student next();}// ConcreteIterator具体迭代器class StudentIterator implements MyIterator {private Student[] students;private int index;public StudentIterator(Student[] students) { this.students students; this.index 0; } public boolean hasNext() { return index students.length; } public Student next() { if (hasNext()) { return students[index]; // 后置自增先返回再后移 } return null; }}// Aggregate聚合接口interface MyIterable {MyIterator createIterator();}// ConcreteAggregate具体聚合类class StudentSystem implements MyIterable {private Student[] students;public StudentSystem(Student[] students) { this.students students; } public MyIterator createIterator() { return new StudentIterator(students); }}// 元素类class Student {private String name;private String number;public Student(String name, String number) { this.name name; this.number number; } public void show() { System.out.println(name number); }}扩展实际项目中的迭代器模式Java 集合框架的 IteratorJava 集合框架是迭代器模式的教科书级实现。所有集合类ArrayList、LinkedList、HashSet、HashMap.entrySet 等都实现了 Iterable 接口返回 Iterator 对象。写过 Java 的人几乎每天都在用它——但很多人没意识到自己在用一个 GoF 模式。增强 for 循环for-each本质上就是编译器帮忙展开的迭代器遍历实现 Iterable 接口的自定义类可以直接用 for-each 语法。List list new ArrayList(List.of(“Java”, “Python”, “Go”));// 显式使用迭代器Iterator it list.iterator();while (it.hasNext()) {System.out.println(it.next());}// for-each 语法编译器展开为迭代器循环for (String lang : list) {System.out.println(lang);} 想看得更透不妨打开 JDK 源码翻两个内部类ArrayList.Itr 正是维护 cursor、lastRet 游标并实现 fail-fast 的那个私有内部类LinkedList.ListItr 还实现了 ListIterator额外支持 hasPrevious()、previous() 双向遍历与 set()、add() 修改元素。一个只往前走一个能来回–迭代器在 JDK 里早已玩出花。fail-fast 机制与安全删除Java 集合的迭代器在遍历时如果发现集合被外部修改就会立刻抛出 ConcurrentModificationException——这就是 fail-fast 机制。原理是迭代器记录了创建时的 modCount每次 next() 都会比对如果直接 list.remove(item) 会改变 modCount迭代器立刻发现不一致就报错。这个机制不属于 GoF 迭代器模式的核心是 Java 集合额外加的安全网但初学者常在这里踩坑。正确的删除方式是使用迭代器自己的 remove()它会同步更新 expectedModCount。List list new ArrayList(List.of(“Java”, “Python”, “Go”));// 错误抛 ConcurrentModificationExceptionfor (String item : list) {if (“Python”.equals(item)) {list.remove(item); // 破坏 modCount}}// 正确用迭代器自己的 remove同步 expectedModCountIterator it list.iterator();while (it.hasNext()) {if (“Python”.equals(it.next())) {it.remove(); // 安全删除}}// Java 8 简洁写法list.removeIf(item - “Python”.equals(item));Stream API 与内部迭代器前面所有写法都是外部迭代器–客户端自己控制 while (it.hasNext()) 循环决定什么时候取下一个、怎么处理。Java 8 的 Stream API 把这套翻了过来集合自己负责遍历调用方只声明要做什么这就是内部迭代器。控制权从客户端反转回集合从怎么遍历跃迁到遍历什么。List langs List.of(“Java”, “Python”, “Go”);// 外部迭代器客户端控制循环自己管 hasNext/nextIterator it langs.iterator();while (it.hasNext()) {String lang it.next();if (lang.length() 3) System.out.println(lang);}// 内部迭代器集合自己遍历调用方只声明动作langs.stream().filter(lang - lang.length() 3).forEach(System.out::println);外部迭代器循环、取值、判断全写在客户端集合只被动提供 hasNext/next。内部迭代器filter、forEach 把行为传进集合集合自己决定怎么走完每个元素。这一层反转带来的不只是写法简洁–并行化变得可能parallelStream 把怎么遍历交给运行时客户端代码一行不改惰性求值也让中间操作可以合并执行。迭代器模式解决如何统一遍历Stream 在它之上多回答了一个问题“遍历时如何把行为也统一起来”。文件目录递归遍历目录是天然的树形结构用迭代器可以把递归遍历的复杂度屏蔽掉——客户端拿到的是一个线性序列。用 Queue 实现 BFS 按层级访问改成 Stack 就是 DFS。这种把遍历策略和访问动作解耦的做法让业务代码专注在对每个文件做什么而不是怎么走到每个文件。JDK 7 的 Files.walk 本质就是这种思路的现代版。class FileIterator implements Iterator {private Queue queue new LinkedList();public FileIterator(File root) { if (root.exists()) queue.offer(root); } public boolean hasNext() { return !queue.isEmpty(); } public File next() { File current queue.poll(); if (current ! null current.isDirectory()) { File[] children current.listFiles(); if (children ! null) { for (File child : children) queue.offer(child); } } return current; }}// 客户端只需 while(it.hasNext()) 即可遍历整棵目录树树形菜单展平后台管理系统里菜单几乎都是树形结构——但渲染、权限检查这些操作往往需要平铺访问每个节点。迭代器可以把树形结构展平成线性序列客户端用 for-each 就能扫过所有节点不用手写递归。用 Stack 实现深度优先用 Queue 换成广度优先遍历策略切换只在迭代器内部。这个套路在 GUI 控件树、组织架构树、评论回复树里都能直接套用。class MenuIterator implements Iterator {private Deque stack new ArrayDeque();public MenuIterator(MenuItem root) { pushChildren(root); // 从根节点的子节点开始 } private void pushChildren(MenuItem item) { // 逆序压栈保证按原始顺序弹出 ListMenuItem children item.getChildren(); for (int i children.size() - 1; i 0; i--) { stack.push(children.get(i)); } } public boolean hasNext() { return !stack.isEmpty(); } public MenuItem next() { MenuItem current stack.pop(); pushChildren(current); // DFS立刻深入子节点 return current; }}组合模式与迭代器模式联动菜单权限检查前面树形菜单展平只看到迭代器怎么把树拉平但那棵树是怎么搭起来的这就轮到组合模式登场了。MenuItem 既是叶子也是容器add 把子菜单挂上来递归建出整棵树迭代器负责把树展平成线性序列权限校验只需对每个节点问一句当前用户有没有这个权限码。三步各司其职组合管结构迭代器管路径业务管动作。// 组合模式统一叶子和容器class MenuItem {private String code; // 权限码private List children new ArrayList();public void add(MenuItem child) { children.add(child); }// 构造、getChildren、getCode 省略}// 组合建树 - 迭代器展平 - 逐节点检查权限MenuItem root buildMenuTree(); // 组合模式搭建树形菜单MenuIterator it new MenuIterator(root); // 迭代器把树展平成线性序列Set userPerms currentUserPerms(); // 当前用户拥有的权限码while (it.hasNext()) { // 逐节点检查MenuItem item it.next();if (!userPerms.contains(item.getCode())) {// 无权限从渲染树里剔除该节点}}迭代器复用前面 MenuIterator 的 DFS 展平逻辑组合模式只管怎么搭树两者衔接代价为零–这也是树形结构场景几乎总是这两个模式成对出现的原因。权限过滤、按钮树渲染、组织架构审批都是同一套组合拳。JDBC ResultSet 封装成标准迭代器JDBC 的 ResultSet 本身就是迭代器的一个变体——rs.next() 移动游标、rs.getXxx() 取列值。但 API 不够简洁受检异常、手动关闭、无法用 for-each。把 ResultSet 包成实现了 Iterator 的类之后客户端就能用增强 for 循环直接消费数据库结果。Spring 的 JdbcTemplate.queryForStream 就采用了类似思路——用迭代器把懒加载的结果集串成 Java 8 的 Stream。class ResultSetIterator implements Iterator {private ResultSet rs;private RowMapper mapper;private boolean hasNextResult;public ResultSetIterator(ResultSet rs, RowMapperT mapper) throws SQLException { this.rs rs; this.mapper mapper; this.hasNextResult rs.next(); // 预取第一行 } public boolean hasNext() { return hasNextResult; } public T next() { try { T item mapper.map(rs); hasNextResult rs.next(); // 预取下一行 return item; } catch (SQLException e) { throw new RuntimeException(e); } }}分页查询的迭代器思想后端开发里分页查询是家常便饭但很少有人把它和迭代器联系起来。仔细一想分页就是迭代器的变体hasNextPage 对应 hasNextgetNextPage 对应 next游标从元素下标变成了页码。把分页器抽象成迭代器后调用方不用关心是查数据库还是调第三方接口统一用 while (pager.hasNext()) 拉取翻页这个动作被彻底屏蔽。class PageIterator implements IteratorList {private int page 1; // 游标当前页码private int size;private PageFetcher fetcher;private List nextBatch; // 预取下一页public boolean hasNext() { return !nextBatch.isEmpty(); } public ListT next() { ListT current nextBatch; page; nextBatch fetcher.fetch(page, size); // 拉取下一页 return current; }}// 大表同步、增量拉取、对接第三方分页接口都能套这个壳