迭代器模式算是设计模式里最“亲民”的一个。日常写 Java 的老哥们,几乎天天都在用它——你自己可能都没意识到。比如随手写一个for (String s : list),底层就是迭代器在工作。但要是接着问一句:Iterator到底怎么实现的?ArrayList里的迭代器和LinkedList的有啥区别?为什么遍历的时候不能随便add、remove?很多人就答不上来了。
这篇文章就以 Java 设计模式第七篇的身份,聊透迭代器模式。不整虚的,从为什么需要它,到核心结构拆解,再到集合框架源码怎么用、实战案例怎么落地、容易踩的坑都有哪些,一次讲清楚。适合刚学设计模式的初级开发者,也适合写了好几年代码想回头巩固一轮的同行,该有的干货都在下面了。
1. 迭代器模式到底解决了什么问题
1.1 一段让人头大的遍历代码
有这样一个场景:某报表系统要从三个不同的数据源取数生成统计结果。MySQL 查询结果是一张表,Redis 缓存是一个列表,某个外部服务返回一个数组。最初的办法很简单,每个数据源写一段独立的遍历逻辑。比如 MySQL 用ResultSet的next(),Redis 用队列的poll(),数组用下标。
代码写多了就发现问题了。报表模块的统计逻辑其实是通用的,无非是求和、求平均、找最大最小值。但因为它被散落在各数据源的遍历代码里,每当新增一个数据源,就得把统计逻辑重新抄一遍。改动风险大、代码冗余多、测试也不好写。
这种问题的根源,是遍历方式被直接暴露在了客户端代码里。客户端必须知道数据是放在数组、列表还是其他什么结构里,还必须知道怎么从里面把元素一个个取出来。一旦底层结构变化,所有相关代码都要跟着改动。
迭代器模式解决的正是这个问题。它把“怎么遍历”这件事,从客户端逻辑中彻底抽离出去。客户端面对的是一个统一的接口:只关心hasNext()和next(),至于底层是数组、链表、Map、文件还是数据库结果集,一概不知。遍历算法封装到了独立的迭代器对象中,由聚合对象负责创建迭代器。这样,客户端代码变得通用稳定,新增数据结构时,只需要提供一个新的迭代器实现,根本不用去动客户端逻辑。
1.2 从生活类比理解迭代器
如果说聚合对象是一本书,那迭代器就是书签。书的内部是纸张、排版、页码,读者不需要关心书是由多少张纸组成的。读者只需要书签告诉他当前读到哪一页,以及还有没有下一页可以翻。
换个角度理解:你每天用手机刷信息流,信息背后的推荐算法、数据存储结构可能千差万别,但呈现给你的就是一个可滑动的内容流。你每次滑动,从内容流里取下一个条目并消费它。这正是迭代器的思想:信息源内部可以很复杂,但对消费者(客户端)而言,只会关心“有没有下一条”和“下一条是什么”这两个问题。
1.3 最适合用迭代器模式的场景
具体到实际开发中,遇到这些情况优先考虑迭代器模式:
- 在需要隐藏内部数据结构的前提下,为外部提供统一的遍历入口
- 需要为同一个聚合对象提供多种遍历方式,比如正序、倒序、跳跃式访问
- 多个聚合对象内部差异很大,但希望对外提供一致的遍历接口
- 底层数据来源频繁变化,但又希望客户端代码保持稳定
如果你们项目中,经常出现遍历逻辑写好几遍、数据源一换代码就崩的情况,那就说明需要迭代器模式来收拾残局了。
2. 核心角色与最小实现
2.1 三个核心角色
迭代器模式在结构上,比很多设计模式都要简单。核心角色就三个:
- 聚合接口(Aggregate):定义创建迭代器对象的方法。Java 中的
Collection接口就是聚合接口的角色,它继承了Iterable<T>,抽象出了iterator()方法。 - 具体聚合类(Concrete Aggregate):具体的数据存储容器,实现聚合接口,并返回一个对应的具体迭代器。例如
ArrayList、LinkedList,它们内部的数据结构完全不同,但都提供iterator()方法。 - 迭代器接口(Iterator):定义访问和遍历元素的方法。经典方法就是
hasNext()和next(),Java 中还有可选的remove(),而ListIterator则扩展了hasPrevious()、previous()、add()等双向遍历方法。
除了这三个角色之外,还有具体迭代器实现类。它们持有聚合对象的内部状态指针,负责控制如何一步步获取元素。在 JDK 中,具体迭代器通常被实现为聚合类的私有内部类,这样可以直接访问聚合类的私有字段,同时又对外部完全隐藏实现细节。
2.2 手写一个最小迭代器
用最直白的方式,手写一个自定义容器并实现迭代器。假设我们有一个ObjectContainer,内部采用数组存储字符串。
先定义接口:
public interface ObjectContainer<E> { ObjectIterator<E> iterator(); } public interface ObjectIterator<E> { boolean hasNext(); E next(); }接口定义好后,实现具体聚合类和具体迭代器:
public class StringContainer implements ObjectContainer<String> { private String[] elements; private int size; public StringContainer(int capacity) { elements = new String[capacity]; } public void add(String element) { elements[size++] = element; } @Override public ObjectIterator<String> iterator() { return new ContainerIterator(); } private class ContainerIterator implements ObjectIterator<String> { private int cursor; @Override public boolean hasNext() { return cursor < size; } @Override public String next() { if (cursor >= size) { throw new NoSuchElementException("No more elements"); } return elements[cursor++]; } } }客户端的使用方式如下:
StringContainer container = new StringContainer(5); container.add("Java"); container.add("Design"); container.add("Pattern"); ObjectIterator<String> iterator = container.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); }注意next()方法中加了越界判断。在真实的集合类实现中,next()越界时抛出NoSuchElementException是标准行为。这个极小示例已经体现了迭代器模式的全部核心:StringContainer将数组存储的细节隐藏起来,客户端只依赖ObjectIterator接口。将来如果把数组换成ArrayList或者链表,只要迭代器实现保持接口不变,客户端代码一行都不用改。
2.3 为什么具体迭代器大多设计成内部类
JDK 集合框架中,具体迭代器基本都是聚合类的私有内部类,比如ArrayList.Itr、LinkedList.ListItr。这么做有一个极其重要的原因:内部类可以无阻碍地访问外部类的私有字段。
ArrayList的迭代器需要直接读取它内部的elementData数组和size字段,如果不做成内部类,这些字段就必须暴露成非私有或提供额外的访问方法,反而破坏了封装性。通过私有内部类,迭代器可以操纵外层集合的私有状态,外部又不直接接触内部实现,这是迭代器模式和封装哲学的共舞。
如果你在自定义容器中要添加迭代器功能,也请优先采用内部类的方式,不要图省事把迭代器写成外部独立类。
3. 深入 Java 集合框架你会发现的事
3.1 编译器如何把 for-each 变成迭代器
从语法层面看,for-each并没有直接调用迭代器,但它借助的是Iterable<T>接口。编译器在编译时会将:
for (String s : list) { System.out.println(s); }改写成类似于下面这段伪代码:
for (Iterator<String> it = list.iterator(); it.hasNext(); ) { String s = it.next(); System.out.println(s); }这也是为什么一个类只要实现了Iterable<T>,就可以直接用于 for-each 循环。所以,当你要为自定义聚合类提供遍历能力时,除了实现迭代器接口,还应该让聚合类实现Iterable<T>,这样客户端就能直接享受到语法糖带来的简洁。
JDK 中Iterator<E>接口从 Java 8 开始增加了forEachRemaining()默认方法,它可以一次性消费剩余全部元素:
List<String> list = List.of("a", "b", "c"); list.iterator().forEachRemaining(System.out::println);这个方法很适合在串行处理中简化代码,当然也要注意,它会一直遍历到结束,不能再中途依赖外层hasNext()做更多控制。
3.2 fail-fast 机制是怎么实现的
有过社区经验的程序员,听到过最多的迭代器异常估计是ConcurrentModificationException。很多刚开始学习集合框架的开发者,会误以为这是并发才会导致的问题。实际上,即使单线程环境下,也会抛出这个异常,因为它和迭代器内部的modCount机制有关。
每个集合类内部都有一个修改计数器的概念,这个计数器在数组列表、链表、哈希表等结构化修改操作中会增加,包括add()、remove()、clear()等。迭代器在初始化时,会将当前的modCount记录为期望值。每次调用next()时,迭代器会检查集合当前的modCount是否和期望值一致。如果不一致,就说明在这个迭代器工作期间集合被修改过了,直接抛出ConcurrentModificationException。
看个例子:
List<String> list = new ArrayList<>(List.of("a", "b", "c")); Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("b")) { list.remove(s); // 这里会抛 ConcurrentModificationException } }执行这段代码时,大概率会在it.next()那行抛出异常。因为list.remove()改了modCount,与迭代器创建时记录的期望值对不上了。
你可能要问:iterator.remove()为什么不抛异常?因为迭代器的remove()方法在执行完删除操作后,会把内部的期望modCount重新同步为集合的最新modCount。这样迭代器认为“修改是我自己干的,下一次next()仍然合法”。其他线程如果修改了集合,迭代器照样会检测到变化。
提示:如果你在遍历时要删除多个元素,请使用
iterator.remove(),不要用集合的remove()。
3.3 不同集合的迭代器有什么差异
迭代器接口统一,但不意味着所有迭代器行为完全一致。实际开发中,这个差异点尤其值得留意:
- ArrayList:迭代器基于数组下标移动,性能最高,
set()可以修改当前元素。它是快速失败型。 - LinkedList:迭代器通过节点指针前进,适合顺序访问。如果频繁进行
get(index)操作性能会很差,此时优先使用迭代器方式遍历。 - HashMap:迭代器遍历的是桶数组和冲突链表,其顺序不能保证与插入顺序一致,若业务依赖迭代顺序就要改用
LinkedHashMap。 - TreeMap:迭代器基于红黑树,按照键的自然顺序或自定义比较器顺序输出。
- CopyOnWriteArrayList:迭代器在创建时基于一个不可变快照数组,遍历期间其他修改不会影响当前迭代,也不会抛
ConcurrentModificationException。代价是每次写入都复制整个数组,适用于读多写少场景。
这里尤其要记住一点:使用HashMap默认迭代器时,不要依赖元素顺序。即使当前感觉有规律,那也是哈希表的随机分布造成的偶然,换一个初始大小或者插入一组不同的 key,顺序可能就完全变了。
3.4 看到 ListIterator 能干什么
ListIterator<E>是迭代器的一个非常重要的强化版本。它支持双向遍历,可以hasPrevious()和previous()反向移动,还允许在遍历期间通过add()在当前位置插入新元素,通过set()修改当前元素。
一个特别实用的场景,是在列表里找某个位置,然后反向输出前面几个元素。比如在操作记录里定位到了某个用户 ID,需要向前查找它前 10 条记录:
List<String> records = getRecords(); ListIterator<String> stringIterator = records.listIterator(records.size()); int count = 0; while (stringIterator.hasPrevious() && count < 10) { System.out.println(stringIterator.previous()); count++; }这个功能通过普通Iterator是做不到的。所以,只看完Iterator还不够,ListIterator也要收入工具箱。
4. 实战:为多源报表模块统一遍历接口
4.1 重构前的痛点
回到开头的报表模块。现在有三个数据源:MySQL 查询结果、Redis 缓存队列、文件行记录。三套数据,三个不同的遍历方式,统计逻辑却是一样的。写代码时只能把统计逻辑复制三遍。
一旦业务人员说“把统计口径改一下”,比如从求平均值改成求中位数,就得同时改三处。改完还要测试三个数据源场景,漏一个线上就出事。这显然是不合理的结构。
4.2 用迭代器模式重新组织
现在用迭代器模式重新设计。定义一个统一的迭代器接口:
public interface ReportDataIterator<E> { boolean hasNext(); E next(); }然后给每个数据源分别实现迭代器:
DatabaseDataIterator:内部包装List<E>,游标从 0 开始,每次next()返回list.get(index++)。CacheDataIterator:内部包装Queue<E>的快照,先把队列元素全部取出放入List,再顺序遍历。这样不会破坏队列原有数据,也便于统一处理。FileDataIterator:内部包装BufferedReader,通过预读取的方式实现hasNext(),每次next()返回一行内容。
有了这三个实现类之后,客户端统计模块就变得统一了。以一个简化版为例:
ReportDataIterator<ReportRecord> iterator = buildIteratorBySource(sourceType); ReportSummary summary = new ReportSummary(); while (iterator.hasNext()) { ReportRecord record = iterator.next(); summary.addAmount(record.getAmount()); summary.countRecord(); } System.out.println(summary.calcAverage());buildIteratorBySource由一个工厂函数根据数据源类型返回对应的迭代器。以后不管接入什么新数据源,只需要新增一种迭代器实现,统计模块完全不需要动。
注意:
FileDataIterator的hasNext()采用预读方式实现时,要特别注意资源释放问题。文件流在遍历结束后,应当在finally块中或者通过实现AutoCloseable来关闭,否则长时间运行的服务会留下大量文件句柄,最终导致无法再读取文件。
4.3 重构之后获得了什么
这次重构之后,核心收益清晰可见:
- 客户端代码大幅简化,统计逻辑只关心
hasNext()和next()两个方法 - 新增数据源只改工厂和新增迭代器实现,不改原有统计流程
- 单元测试容易写,因为迭代器是接口,测试中可以直接传入一个基于内存列表的迭代器,不再依赖数据库、Redis 或文件
- 逻辑通用了,将来如果要对接消息队列、对象存储,成本都很低
在这个场景中可以看到,迭代器模式并不是什么高深的理论,它本质上是对变化数据源的一种抽象。有统一遍历需求的地方,就有迭代器模式的用武之地。
5. 迭代器模式的变化形态
5.1 内部类私有迭代器与匿名类包装
JDK 中大多数集合的迭代器,都是以私有内部类的形式存在,这是面向封装的经典实现。业务代码中如果只是简单包装一个现成的集合,也可以考虑用匿名内部类实现。例如,想让外部只能遍历一个受控子集时,可以用包装迭代器来达到目的:
public Iterator<String> limitedIterator(int limit) { Iterator<String> source = fullList.iterator(); return new Iterator<>() { private int count; @Override public boolean hasNext() { return count < limit && source.hasNext(); } @Override public String next() { if (!hasNext()) { throw new NoSuchElementException(); } count++; return source.next(); } }; }这种包装迭代器,在装饰器场景中特别常见,优势在于不用改动原来的数据源,就能在当前迭代中限制数量、过滤元素或增加行为。
5.2 反向迭代器与过滤迭代器
业务中经常需要倒序遍历,尤其在一些报表或者时间线场景。反向迭代器内部,游标从尾部开始,逐步往前移动:
private class ReverseIterator implements Iterator<E> { private int cursor = size - 1; @Override public boolean hasNext() { return cursor >= 0; } @Override public E next() { return elements[cursor--]; } }过滤迭代器则是将过滤逻辑封装到hasNext()里,不断跳过不符合条件的元素。比如空值过滤:
@Override public boolean hasNext() { while (cursor.hasNext()) { E candidate = cursor.next(); if (candidate != null) { pending = candidate; return true; } } return false; } @Override public E next() { if (pending == null && !hasNext()) { throw new NoSuchElementException(); } E result = pending; pending = null; return result; }这样的迭代器非常优雅,因为客户端遍历的时候,过滤逻辑已经被悄然抽走了。
5.3 惰性求值与分页迭代器
迭代器模式最值得深度挖掘的变体,之一是“惰性迭代器”。它不在创建时一次性加载全部数据,而是在每次调用next()时才去底层取数据。这在数据库查询、远程接口调用等场景中价值极高。
比如实现数据库分页查询迭代器,每次从数据库取一批记录,迭代完当前批次再去查下一页:
public class PageIterator implements Iterator<Record> { private final PageFetcher fetcher; private List<Record> currentBatch; private int indexInBatch; private int pageNo = 0; private boolean hasMore = true; public PageIterator(PageFetcher fetcher) { this.fetcher = fetcher; } @Override public boolean hasNext() { if (currentBatch == null || indexInBatch >= currentBatch.size()) { if (!hasMore) { return false; } currentBatch = fetcher.fetchPage(pageNo++); indexInBatch = 0; if (currentBatch.isEmpty()) { hasMore = false; return false; } } return true; } @Override public Record next() { if (!hasNext()) { throw new NoSuchElementException(); } return currentBatch.get(indexInBatch++); } }这个迭代器能把海量数据的遍历压力均匀分摊到访问过程中,避免一次性将所有数据加载到内存。实现时最关键的,是把边界条件处理好,尤其是“某一页刚好为空”时需要正确中止循环,否则会出现无限拉取空页的问题。
6. 迭代器模式与其它模式的关系
6.1 快速对比表
迭代器模式常与几个模式放在一起比较,因为它们表面上都在处理对象访问关系:
| 模式 | 类型 | 核心关注点 | 典型场景 |
|---|---|---|---|
| 迭代器模式 | 行为型 | 顺序访问聚合对象,隐藏内部表现 | 集合遍历、统一数据源遍历 |
| 组合模式 | 结构型 | 树形结构的递归组合与访问 | 菜单树、目录树 |
| 工厂方法模式 | 创建型 | 对象创建延迟到子类 | 为不同集合生成不同迭代器 |
| 访问者模式 | 行为型 | 在对象结构不变的前提下增加操作 | 复杂对象结构的编译类操作 |
| 策略模式 | 行为型 | 算法的封装与替换 | 不同遍历方式的运行时切换 |
最简单的判断方式:如果需求焦点是“怎么让客户端统一取到元素”,那就是迭代器模式;如果焦点是“在取到元素后对对象执行什么新操作”,那可能要引入访问者模式。两者不是替代关系,而是解决不同层面的问题。
6.2 与组合模式配合构建树形遍历
组合模式中,树形结构由叶子节点和组合节点组成。组合节点还可以包含子节点,呈现递归结构。如果直接用递归方法遍历菜单树,代码会很直接,但也意味着客户端需要了解树的结构并自行递归。
如果把遍历的逻辑封装成迭代器,客户端只需要拿到树根的迭代器,然后像遍历普通列表一样完成深度优先遍历。实现上,可以借助一个栈来保存待访问节点:
public class MenuIterator implements Iterator<MenuComponent> { private final Deque<MenuComponent> stack = new ArrayDeque<>(); public MenuIterator(MenuComponent root) { stack.push(root); } @Override public boolean hasNext() { return !stack.isEmpty(); } @Override public MenuComponent next() { if (!hasNext()) { throw new NoSuchElementException(); } MenuComponent current = stack.pop(); if (current instanceof Menu) { List<MenuComponent> children = ((Menu) current).getChildren(); for (int i = children.size() - 1; i >= 0; i--) { stack.push(children.get(i)); } } return current; } }这种用迭代器包装递归遍历的手法,在一部分框架源码中很常见,它将树形结构的访问细节彻底隐藏。
6.3 与 Stream API 的关系
Java 8 之后,很多人觉得Stream已经取代了迭代器。实际上,Stream内部还是大量依赖了迭代器机制。比如Stream的iterator()方法,以及Spliterator这个专为并行流设计的可拆分迭代器。
如果业务只是简单转换和聚合数据,直接用Stream更合适。但如果是自定义数据结构、遍历逻辑复杂、需要延迟加载或者希望客户端能复用同一套遍历协议,那手写迭代器依然更贴切。两者并不矛盾,更像不同层次的抽象工具。
7. 常见问题与排查技巧
7.1 六种高频错误与解决方式
迭代器模式本身不复杂,但实际使用中的坑却不少。把这些高频错误整理如下:
第一,遍历中直接修改集合。使用集合自身的remove()或add()方法时,极大概率触发ConcurrentModificationException。解决办法是使用迭代器自身的remove()方法,或者收集待删除的元素,遍历结束后统一删除。
第二,迭代器状态残留。同一个迭代器实例,游标是连续移动的。如果需要重新遍历,必须重新从聚合对象那里获取新迭代器,不要复用旧的。
第三,空集合处理不当。如果聚合对象本身为空,hasNext()应该返回 false,而不是继续尝试取元素。否则,迭代逻辑进入死循环或者出现空指针。
第四,next()忽略越界检查。不管客户端怎么调用next(),实现都应当做好越界保护,否则默认抛出比较晦涩的数组越界异常。正确的做法是抛出NoSuchElementException,让异常语义更明确,排查更容易。
第五,IO 资源未释放。包装输入流、数据库连接等资源的迭代器,如果没有在遍历结束时关闭资源,会造成连接泄漏。建议迭代器实现AutoCloseable,或者由外层在finally中负责关闭底层资源。
第六,并发环境乱用普通迭代器。多个线程同时使用同一集合的普通迭代器,容易产生不可预知的结果。要根据场景选择CopyOnWriteArrayList或者ConcurrentHashMap的并发迭代器。
7.2 for-each 与显式迭代器怎么取舍
for-each在编译后本质是迭代器调用。那么遇到实际问题时到底选哪种?
我的经验是:
- 只是从头到尾把元素全走一遍,用
for-each最简洁 - 需要在遍历过程中删除当前元素,用显式迭代器的
remove() - 需要反向遍历或跳跃访问,使用
ListIterator或者自定义迭代器 - 需要在遍历中间做更复杂的短路判断,
while (iterator.hasNext())的显式方式控制力更强
另外要留意:for-each中如果自定义集合的迭代器是基于modCount实现的快速失败型,那么任何结构修改都会抛异常。如果是CopyOnWriteArrayList的弱一致迭代器,就不会有这个问题。明白底层容器语义再排错,思路会清晰很多。
7.3 掌握一个自定义迭代器模板
写自定义容器时,可以直接套用下面这个模板,它和 JDK 的内置实现思路一致:
public class CustomCollection<E> implements Iterable<E> { private Object[] data; private int size; private int modCount; public boolean add(E element) { if (size == data.length) { // 扩容逻辑 } data[size++] = element; modCount++; return true; } @Override public Iterator<E> iterator() { return new CustomIterator(); } private class CustomIterator implements Iterator<E> { private int cursor; private int expectedModCount = modCount; @Override public boolean hasNext() { return cursor < size; } @Override public E next() { checkForModification(); if (cursor >= size) { throw new NoSuchElementException(); } return (E) data[cursor++]; } @Override public void remove() { if (cursor <= 0) { throw new IllegalStateException("next() must be called before remove()"); } checkForModification(); // 这里完成集合元素移除逻辑 modCount++; expectedModCount = modCount; cursor--; } private void checkForModification() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } } }这个模板的关键点有两个:一是快速失败检查的退避时机,二是在remove()后要重新同步期望值。很多自定义迭代器不写这些,就会出现各种诡异的异常或行为不一致。
8. 项目实践中的体会
8.1 在数据适配层做迭代器
有一回我在做一个订单导出功能,数据来源有内存缓存、数据库分页查询、消息队列备份三种。导出模块需要把不同来源的数据统一写入 CSV 文件。最初实现充满了 if-else 分支,每个分支用不同方式遍历数据。
重构后,我为每个数据源实现了一个迭代器,导出模块只接受Iterator<Order>作为参数,遍历并写入文件。之后新增了一个数据源,新增迭代器实现类就好,导出模块代码零改动,这是迭代器模式非常有价值的地方。
这个案例也说明了,在数据处理流程相对固定、但数据来源多变的系统中,迭代器模式是最自然的选择。
8.2 在单元测试中的价值
我特别推崇迭代器模式的另一个原因,是它对测试非常友好。因为迭代器是接口,测试时可以轻松用List.iterator()、Collections.emptyIterator()等现成实现替代复杂的真实数据源。
例如,某功能需要从文件数据源统计关键字出现次数。写测试时,直接构造一个内存列表的迭代器传进去:
Iterator<String> fileData = List.of("apple", "banana", "apple").iterator(); KeywordCounter counter = new KeywordCounter(); int result = counter.count(fileData, "apple"); assertEquals(2, result);这样测试的焦点完全落在业务逻辑上,不用准备真实文件、启动数据库、连 Redis。对快速迭代和回归测试极其友好。
8.3 关于并发和性能的一点建议
如果你要遍历的数据量极大且性能高度敏感,建议考虑批量迭代接口、使用原始类型数组,或者采用缓存友好的顺序访问策略。迭代器模式在常规业务中的性能开销可以忽略,但在千万级数据扫描时,每一次接口调用和游标移动都可能被放大。此时,代码可读性和极致性能之间需要做一个权衡。
并发编程中,优先使用 JDK 并发包提供的迭代器,而不是自己实现同步机制。CopyOnWriteArrayList适合读多写少场景,ConcurrentHashMap的迭代器是弱一致性,能在遍历期间反映部分更新,但不提供强一致快照。
提示:在高并发写入的缓存场景,
ConcurrentHashMap的弱一致迭代器更合适;在实时性要求不高的配置读取或报表快照场景,CopyOnWriteArrayList是更稳妥的选择。
9. 收尾:把迭代器用成习惯
迭代器模式虽然是入门级的设计模式,但想写得好、写得稳,还得在真实代码里反复打磨。比起花里胡哨的天马行空,这个模式更讲究贴合实际需求的抽象。
我个人最深的体会是:当遍历逻辑开始反复出现、数据来源不断变化时,停下来先想想,能不能用迭代器模式把遍历行为抽离出来。大多数情况下,这样一个动作就能让代码结构明显上了一个台阶。另外,如果你在实现自定义容器,务必尽早实现Iterable<E>,这能让客户端无缝使用 for-each 和 Stream,体验完全不一样。
如果想进一步巩固,可以自己动手做三件小事:给自定义链表实现一个迭代器,做一个支持双向遍历和过滤的可迭代容器,再写一个懒加载的分页迭代器。这三件练完,迭代器模式基本就被你拿捏了。
最后分享一个心得:设计模式的应用,常常不是一蹴而就的改造,而是伴随迭代逐渐沉淀出来的。第一次写迭代器可能并不完美,但先让结构对了,再逐步完善健壮性和性能,在回头审视代码的过程中,往往能比直接抄一份现成实现领悟更多东西。