资讯动态

迭代器模式深度解析:从集合框架源码到并发修改的坑

发布时间:2026/10/14 1:10:13 来源:尧图企业网站定制
迭代器Iterator可能是23种设计模式里最隐形的一个——你看不到它但它每天都在干活。写过几天Java的人基本都用过增强for循环也知道Iterator接口的存在但真正问起迭代器模式到底解决了什么问题、为什么集合框架非要这么设计能讲清楚的人其实不多。这篇东西我用实际源码和手写代码展开讲从集合框架的设计初衷聊到二叉树遍历从fail-fast机制聊到并发修改的坑争取一次把迭代器模式讲透。适合谁看准备面试的初中级Java开发者、写业务代码想提升抽象能力的后端工程师还有刚学设计模式觉得这模式有啥用的同学。迭代器模式不挑语言但本文的示例全部基于Java版本以JDK 8为主源码层面会提到JDK 17的部分实现不影响理解。1. 迭代器模式到底是干什么的1.1 从一段最普通的代码说起ListString list new ArrayList(); list.add(A); list.add(B); list.add(C); for (int i 0; i list.size(); i) { System.out.println(list.get(i)); }这段代码很常见但它的坏味道藏在细节里调用方知道list是ArrayList知道它有get(int)方法还知道size()的含义。换句话说调用方和集合的具体实现绑死了。哪天你要把它换成LinkedListget(i)就从O(1)变成O(n)两层循环就是O(n²)的灾难。迭代器模式要解决的就是这种耦合。它把怎么存储和怎么遍历拆开集合只管内部结构遍历的逻辑全部收敛到迭代器对象里。调用方只依赖一个统一的Iterator接口根本不关心背后是数组、链表还是树。ListString list new ArrayList(Arrays.asList(A, B, C)); IteratorString it list.iterator(); while (it.hasNext()) { System.out.println(it.next()); }你可能会说这不就是换个写法吗表面看是但本质区别是第二段代码里调用方对集合的认知从ArrayList下降为Iterable。替换实现类的时候一行代码都不用改。这就是面向接口编程的意义——迭代器模式是它的样板间。1.2 四个角色与一个核心契约迭代器模式的标准结构由四部分组成角色职责Java中的对应迭代器接口Iterator定义访问、遍历元素的抽象方法java.util.Iterator具体迭代器ConcreteIterator实现遍历算法记录当前游标位置集合内部类如ArrayList.Itr聚合接口Aggregate定义创建迭代器的方法java.lang.Iterable具体聚合ConcreteAggregate返回具体的迭代器实例ArrayList、HashSet等核心契约在Iterator接口上JDK迭代器有三个方法必须说清楚public interface IteratorE { boolean hasNext(); // 是否还有下一个元素 E next(); // 返回当前元素并移动游标 default void remove() { throw new UnsupportedOperationException(remove); } }hasNext()和next()是配对出现的缺一不可。光有next()没有hasNext()你遍历时就不知道边界在哪要么越界要么死循环。这两个方法的存在规定了迭代器是一个单向、只进不退的游标它只记得当前走到哪了不记得之前走过什么。注意next()和remove()的顺序有讲究。必须先next()再remove()删除的是刚被next()返回的那个元素而不是游标当前指向的元素。这个细节很多人踩过坑后面专门讲。2. 为什么说迭代器是集合框架的幕后总导演2.1 Iterable和Iterator一对容易被搞混的父子面试里高频问题之一Iterable和Iterator什么区别一句话版本Iterable是集合的能力声明意思是我这个集合可以被遍历Iterator是具体的遍历工具负责真正干活。集合实现Iterable接口返回一个Iterator实例给你用。public interface IterableT { IteratorT iterator(); default void forEach(Consumer? super T action) { ... } }这里有个容易忽略的设计细节为什么不让集合直接实现Iterator因为那样集合就自带游标状态了——意味着同一个集合对象同时只能有一个遍历在进行而且集合自己得维护当前遍历位置和它的存储逻辑纠缠在一起。把遍历状态封装到独立的迭代器对象里多个迭代器可以互不干扰地同时遍历同一个集合。你想想一个ArrayList套两层for循环各拿各的iterator()能正常跑靠的就是这个设计。2.2 增强for循环其实是个语法糖很多人写了几年for (String s : list)不知道背后被编译器改成了什么。看编译后的字节码就明白增强for循环对Iterable类型会被展开成这样的逻辑for (IteratorString it list.iterator(); it.hasNext(); ) { String s it.next(); // 循环体 }所以增强for循环不是JVM原生支持的魔法它就是iterator()hasNext()next()的语法糖。这也解释了为什么数组不能直接放进Iterable——数组虽然能用增强for但编译器对数组走的是另一种展开方式基于下标遍历。理解了这层关系你就能解释很多奇怪的现象为什么在增强for循环里给集合add会抛ConcurrentModificationException——因为底层迭代器察觉到了结构性修改。为什么自己写的类实现了Iterable之后突然就能用增强for了——因为编译器认这个接口。为什么有些集合比如LinkedList的迭代器遍历比get(i)快很多——因为迭代器是顺藤摸瓜而get(i)每次都要从头找。2.3 迭代器模式带来的三个价值把这些问题归结起来迭代器模式的实际价值有三条第一解耦存储与遍历。集合内部是数组、链表、散列表还是红黑树调用方一概不关心。这是它对开闭原则的贡献新增一种集合类型只要实现Iterable并提供一个迭代器所有依赖迭代器的代码都能立即工作。第二统一遍历接口。不同数据结构有不同的遍历节奏但对外都收敛为hasNext()/next()两个方法。这也给上层的高级操作铺了路——Collections.sort、Stream的spliterator都建立在这层统一抽象之上。第三支持多种遍历方式。如果你想让同一个集合既能顺序遍历又能倒序遍历、既能按深度优先又能按广度优先不需要改集合本身多写几个迭代器类就行。迭代器模式天然把遍历算法的扩展和集合结构解耦这是其他遍历方式比如直接暴露get(i)做不到的。3. 从零手写一个迭代器把原理彻底落地3.1 第一步定义一个最简单的聚合接口理论讲再多不如动手写。我带你从零实现一个极简的书架类它内部用数组存书对外提供迭代器。先用最原始的版本public class BookShelf implements IterableBook { private Book[] books; private int size 0; public BookShelf(int maxSize) { this.books new Book[maxSize]; } public void addBook(Book book) { if (size books.length) { throw new IllegalStateException(书架满了); } books[size] book; } public int getSize() { return size; } Override public IteratorBook iterator() { return new BookShelfIterator(); } }Book就是个简单的POJO有title和author两个字段就行。这里BookShelf只实现了Iterable没有暴露get(int)——这正是迭代器模式的态度外部只能通过迭代器访问内部细节全部隐藏。3.2 第二步实现具体迭代器用内部类还是匿名类BookShelfIterator最关键。它必须维护游标位置实现hasNext()和next()private class BookShelfIterator implements IteratorBook { private int index 0; Override public boolean hasNext() { return index size; } Override public Book next() { if (index size) { throw new NoSuchElementException(没有更多元素了); } return books[index]; } }两个设计细节值得注意一是这个迭代器用成员内部类实现而不是外部独立类。为什么因为它需要直接访问BookShelf的私有字段books和size。用内部类就不用额外传参也不用写getter代码最简洁。JDK里ArrayList.Itr也是这么干的——内部类是集合类实现迭代器的事实标准。二是在next()里做了边界检查。有人觉得多余我都有hasNext()了调用方还能越界真能。比如调用方写it.next()但不先调hasNext()或者两个线程共用一个迭代器都会走到越界分支。规范里next()的行为是没有元素时抛NoSuchElementException这是Iterator接口文档明确要求的照做就行。BookShelf shelf new BookShelf(5); shelf.addBook(new Book(Java编程思想, Bruce Eckel)); shelf.addBook(new Book(Head First设计模式, Eric Freeman)); IteratorBook it shelf.iterator(); while (it.hasNext()) { Book b it.next(); System.out.println(b.getTitle() - b.getAuthor()); }跑一下就能看到两本书被打印出来。到这里一个最小可用的迭代器模式就落地了。你自己实现的BookShelf能用在增强for循环里因为实现了Iterable。3.3 第三步复杂迭代器——二叉树的中序遍历数组的迭代器太简单体现不出模式威力。换个场景二叉树。你想要一个迭代器能按中序遍历左-根-右输出所有节点而且不暴露树的节点是怎么组织的。public class BinaryTreeT implements IterableT { private TreeNodeT root; private static class TreeNodeT { T value; TreeNodeT left; TreeNodeT right; TreeNode(T value) { this.value value; } } Override public IteratorT iterator() { return new InOrderIterator(); } private class InOrderIterator implements IteratorT { private final DequeTreeNodeT stack new ArrayDeque(); private TreeNodeT current; InOrderIterator() { this.current root; } Override public boolean hasNext() { return current ! null || !stack.isEmpty(); } Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } while (current ! null) { stack.push(current); current current.left; } TreeNodeT node stack.pop(); current node.right; return node.value; } } }这里用的是迭代版中序遍历用显式栈模拟递归。注意几个实现要点hasNext()判断的是当前节点不为空或栈不为空两个条件缺一不可。只判断栈不为空会漏掉当前节点本身。next()里的while循环负责一路压入左子树把最左边的节点弹出来返回再把游标切到右子树。这个算法网上讲烂了但配合迭代器写出来你就能体会到状态封装的意义——调用方每次只问有没有下一个复杂的遍历状态全在迭代器内部维护。迭代器是一次性的。遍历完hasNext()为false后这个迭代器就废了不能重置。标准Iterator接口设计如此想重新遍历就调用集合的iterator()再拿一个新的。这个例子的意义在于迭代器模式并不局限于数组下标这种简单结构。任何有顺序地访问所有元素的需求——树、图、嵌套集合——都可以用迭代器封装。你甚至可以做一个ReverseIterator从右到左遍历集合类本身只加一个方法reverseIterator()就够了现有的所有遍历代码不受任何影响。4. 实战中迭代器模式还能怎么用4.1 场景一自定义业务集合对象真实业务里你自己的领域对象集合往往不是简单的List。比如权限系统里的菜单树每个菜单有子菜单你想让外部代码能用增强for循环遍历所有菜单节点而不用暴露树的内部结构Menu root buildMenuTree(); for (Menu menu : root) { System.out.println(menu.getName()); }只要让Menu实现IterableMenu返回一个广度优先的迭代器外部代码就能拿到这种看起来像集合的体验。这对代码的可读性提升非常明显——调用方不需要知道树是怎么遍历的它只关心给我所有菜单。我在实际项目中见过类似的实现报表模块的数据源包含多个Sheet、每个Sheet有多个行列团队给数据源实现了一个迭代器外部统一用hasNext()/next()读取单元格。这样上层导出逻辑完全不用关心数据源来自Excel、数据库还是接口返回想切换数据源只换迭代器就行。这是迭代器模式和策略模式的合体应用本质还是存储与遍历解耦。4.2 场景二惰性迭代与分页遍历迭代器的另一个大杀器是惰性求值——元素不是一次性全部加载而是用多少取多少。常见的例子是数据库分页查询public class PagedQueryIteratorT implements IteratorT { private final PagedDataFetcherT fetcher; private final IteratorT currentPage; private int page 0; private boolean hasMore true; public PagedQueryIterator(PagedDataFetcherT fetcher, int pageSize) { this.fetcher fetcher; this.currentPage fetcher.fetch(page, pageSize).iterator(); this.hasMore currentPage.hasNext(); } Override public boolean hasNext() { if (currentPage.hasNext()) { return true; } if (!hasMore) { return false; } page; ListT nextData fetcher.fetch(page, pageSize); if (nextData.isEmpty()) { hasMore false; return false; } // 这里需要重新构造currentPage实际实现会用一个可替换的内部迭代器 return true; } Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } return currentPage.next(); } }这个思想在真实框架里很常见——数据量太大时一次性把全部数据放进内存既不现实也没必要。用迭代器做惰性加载调用方永远只感知到一个可以遍历的东西但背后可能每取100条才查一次数据库。这是迭代器模式的进阶玩法理解了这个你再看Stream API的中间操作就顺了——它们本质上也是惰性迭代思想的延伸。4.3 场景三统一多个异构数据源我给某项目做过一个数据同步模块数据源有三种数据库查询结果、远程接口返回的JSON数组、本地缓存的CSV解析结果。三种数据的结构完全不同但对外要统一提供一个逐条读取的接口。做法就是给每种数据源各写一个迭代器DbResultIterator包装ResultSetnext()时把当前行转成统一对象。ApiResponseIterator内部维护游标和数组next()时从解析好的JSON数组取元素。CsvFileIterator按行读取每次next()解析一行CSV。对外暴露的DataSource接口就一个方法IteratorRecordDTO iterator()。上层同步引擎拿到迭代器后什么都不用改就能处理所有类型的数据源。后来加一个新的消息队列数据源只写了一个MqMessageIterator同步引擎一行代码没动。这就是迭代器模式在架构层面的价值它让你的系统天然具备可扩展的数据接入能力而且扩展成本极低。很多框架设计本质就是这种思路——你实现一个接口Iterator、Handler、Adapter框架负责在合适的时候调用你至于框架内部怎么组织数据你不知道也不用知道。5. 迭代器的坑踩过一次就长记性5.1 ConcurrentModificationException是怎么来的最常见的迭代器问题就是在遍历时改动集合。比如这段代码ListString list new ArrayList(Arrays.asList(A, B, C, D)); for (String s : list) { if (s.equals(B)) { list.remove(s); // 运行时抛 ConcurrentModificationException } }很多人不理解我只是删了一个元素为什么会报并发修改异常明明没有多线程啊。答案藏在ArrayList的迭代器内部。ArrayList有一个modCount字段记录结构被修改的次数add、remove、clear都会加1。迭代器在初始化时把这个值记下来private class Itr implements IteratorE { int expectedModCount modCount; // 迭代器创建时的快照 ... public E next() { checkForComodification(); ... } final void checkForComodification() { if (modCount ! expectedModCount) throw new ConcurrentModificationException(); } }每次next()之前迭代器都会对比modCount和expectedModCount。你的list.remove(s)让modCount变了但迭代器里的expectedModCount还是旧值一比对不一致立刻抛异常。这个机制叫fail-fast——快速失败宁可抛出异常也不让你在不可预知的状态里继续跑下去。必须声明一下fail-fast不是绝对的线程安全保证。它是为了尽早暴露bug设计的两个线程同时改集合可能一个不抛异常但读到脏数据也可能抛异常。真正多线程场景应该用CopyOnWriteArrayList或ConcurrentHashMap它们的迭代器是弱一致的不抛这个异常。5.2 正确删除方式用迭代器自己的remove既然遍历时不能直接调集合的remove那该怎么删用迭代器自带的remove()IteratorString it list.iterator(); while (it.hasNext()) { String s it.next(); if (s.equals(B)) { it.remove(); } }关键在于ArrayList.Itr的remove()方法在删除后会把expectedModCount重新赋值为modCount两者保持一致所以不会抛异常。这是迭代器接口里remove()的真正用途——在遍历过程中安全地删除刚被next()返回的元素。JDK官方文档对remove()的描述很有意思Removes from the underlying collection the last element returned by this iterator。注意last element returned这个措辞意思是你得先next()再remove()想一次remove()两次next()返回的元素不行第二次remove()也会抛IllegalStateException——因为它记录的上一次返回的元素已经被删掉了。实际开发中我更推荐直接用removeIfJava 8之后这个东西太好用了list.removeIf(s - s.equals(B));内部实现就是迭代器遍历加remove()但代码简洁得多也不容易写错。5.3 增强for循环、迭代器与fori性能对比经常有人问三种遍历方式性能差别大吗我直接给结论不卖关子对ArrayListget(i)最快数组随机访问O(1)增强for循环迭代器略慢一点点但差距在百万级元素内通常可以忽略。对LinkedListget(i)是O(n)for i整体O(n²)是灾难迭代器是O(n)必须用迭代器或增强for。对HashSet等非List集合根本没有get(i)只能用迭代器/增强for/stream。所以通用规则是能用迭代器就用迭代器别用下标循环。下标循环会让你下意识地依赖集合是List这个事实一旦换实现类就出事。写代码的时候把面向接口这四个字刻在脑子里迭代器是它最好的练习场。5.4 Stream和迭代器怎么选Java 8之后很多人用Stream替代了手动迭代list.stream().filter(s - !s.equals(B)).collect(Collectors.toList()); list.forEach(System.out::println);Stream底层其实也是基于迭代器或Spliterator实现的。选哪个我的经验是只做遍历、不玩链式操作forEach或增强for直观简单。需要过滤、映射、聚合Stream更语义化可读性远好于手写循环加if。需要中途停止或跳过元素迭代器的hasNext()控制流更灵活。性能敏感且数据量大迭代器通常开销更低Stream的抽象层和lambda调用有一定成本。但不管选哪种理解迭代器模式本身依然是值得的——它是整个集合框架和Stream API的地基。地基不稳上层再漂亮也是空中楼阁。6. 迭代器模式在设计模式大家庭里的位置6.1 和相关模式的区分学设计模式最容易混淆的几个我帮你理一遍迭代器 vs 策略策略模式是封装算法族让算法可以互相替换迭代器封装的是遍历算法从这个角度算策略的一种特例。但迭代器有自己的角色划分和接口契约所以单独列为一种模式。迭代器 vs 组合组合模式Composite讲的是树形结构中部分与整体的关系它常和迭代器配合使用——对组合结构做遍历时迭代器负责屏蔽树的层次差异。菜单树的例子就是两者结合。迭代器 vs 访问者访问者模式关注在不改变元素类的前提下为元素增加新操作它也需要遍历集合。迭代器管怎么走到每个元素访问者管到了之后对元素做什么两者天然互补。理解这些区别后你会发现设计模式不是孤立的教条它们像乐高零件一样互相咬合。迭代器模式的关键词是游标和解耦抓住这两个词它的定位就清晰了。6.2 什么时候该用、什么时候别用该用的场景集合有多种遍历方式顺序、倒序、深度优先等且不想把这些逻辑塞进集合类里。需要对外隐藏集合的内部数据结构。想要统一遍历异构数据源。实现惰性加载避免一次性把全部数据拉到内存。不该硬用或者要斟酌的场景集合本身就极其简单比如就几个固定元素外部直接访问字段更清晰硬套模式是过度设计。集合类是内部私有使用没有对外扩展和替换的需求——模式的价值很大程度在于面向未来如果确定没有未来不搞。判断标准就一句有没有可能出现第二种遍历方式或者第二种集合实现如果有迭代器模式值得考虑如果没有写个普通方法遍历就行。我在实际项目里见过把迭代器模式用在只有两种元素的配置对象上硬写了三个类——那属于为了模式而模式反而伤害了可读性。设计模式是工具不是奖状该用的时候用不该用的时候别硬凑。7. 最后分享一点我的使用体会迭代器模式是我认为最适合作为设计模式入门第一课的模式之一因为它足够简单——四个角色、两个方法10分钟就能看懂结构但它的影响足够深远——整个Java集合框架、Stream API、增强for循环的语法糖全部建立在这一层抽象上。理解了它你再看集合源码时会有一种原来如此的通透感。我自己写代码时有个习惯凡是涉及遍历一组东西的地方先问一句这组东西以后会不会换个存储方式调用方是否需要感知存储细节只要答案是会或不需要我就让这个类实现Iterable把遍历工作交给迭代器。这个习惯帮我挡掉了不少后期重构的麻烦。如果你正在学设计模式我建议别只背定义。拿一个自己项目里的真实集合类按这篇文章的步骤手写一个迭代器再把它换成二叉树、文件行、数据库分页各写一遍。写完之后你自然就懂迭代器模式的价值了——纸上得来终觉浅这个模式尤其如此。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑