
这一篇的迭代器模式(Iterator),可能是所有设计模式里你用得最多、却最没意识到自己在用模式的一个——因为每次你写for (Order order : orders)这样的增强 for 循环时,背后就是它在工作。它已经被彻底融进了 Java 语言本身。迭代器模式解决的问题一句话:提供一种统一的方式来遍历一个集合,而不暴露集合的内部结构。关键词是统一和不暴露内部。想想看,ArrayList内部是数组,LinkedList内部是链表,HashSet内部是哈希表——它们的存储结构天差地别,遍历方式本该完全不同(数组按下标、链表顺指针、哈希表按桶)。可你在用的时候,却能用完全一样的for-each语法遍历它们所有。这份无论内部是什么结构,遍历代码都一样的统一体验,正是迭代器模式给的。它把怎么遍历这件事,从集合内部抽出来,封装成一个独立的迭代器对象。我们的订单场景里,举个需要自定义集合的例子:假设我们做了一个OrderList,内部可能用数组存订单,也可能哪天改成链表。我们希望使用方能用统一的方式遍历它,而且即使内部结构以后从数组换成链表,遍历它的代码也一个字都不用改。迭代器模式就是来保证这一点的。因为这个模式在 Java 里已经是内置基础设施,这一篇我会讲得比前几篇稍简洁:重点讲清它的核心思想、为什么 Java 要内置它、以及你天天用的for-each和fail-fast背后的门道,而不必像别的模式那样铺很长。这篇文章按这条线索展开:先看遍历逻辑和集合搅在一起的问题;再讲迭代器如何把遍历分离出来;然后看 Java 内置的Iterable/Iterator和for-each的关系;接着讲一个天天遇到、却未必懂原理的fail-fast机制;最后给出适用边界。贯穿例是自定义订单集合。目录遍历逻辑和集合搅在一起的问题迭代器模式:把怎么遍历分离出去Java 内置的迭代器:for-each 的真相一个天天遇到的机制:fail-fast什么时候用迭代器模式一、遍历逻辑和集合搅在一起的问题假设我们自己写了个订单集合OrderList,内部用数组存:publicclassOrderList{privateOrder[]ordersnewOrder[100];privateintsize0;publicvoidadd(Ordero){orders[size]o;}publicOrder[]getOrders(){returnorders;}// 把内部数组暴露出去publicintgetSize(){returnsize;}}使用方想遍历它,只能这样:OrderListlist...;Order[]arrlist.getOrders();// 拿到内部数组for(inti0;ilist.getSize();i){// 按下标遍历Orderoarr[i];// ...}这段代码有两个大问题:暴露了内部结构:使用方必须知道OrderList内部是个数组、要用下标遍历。这违反了封装——内部实现细节泄露给了外面。更糟的是,getOrders()把内部数组直接交了出去,外面甚至能随意篡改它。遍历方式和内部结构绑死:哪天OrderList内部从数组改成链表(为了频繁插入删除),那所有按下标遍历的使用方代码全部作废,得改成顺着指针遍历。集合的内部重构,波及了所有遍历它的地方。问题的根源是:如何遍历这件事,和集合内部怎么存储耦合在了一起,还暴露给了使用方。我们真正想要的是:让集合提供一个迭代器,使用方只通过这个迭代器来遍历(问它还有下一个吗?“下一个是谁?”),完全不用知道内部是数组还是链表;哪天内部结构变了,只需改迭代器的实现,使用方代码一个字不动。这就是迭代器模式。二、迭代器模式:把怎么遍历分离出去迭代器模式的做法:定义一个迭代器接口(通常就两个方法:hasNext()判断还有没有、next()取下一个);集合提供一个方法返回它的迭代器;使用方只跟迭代器打交道。第一步,迭代器接口(Java 已内置java.util.Iterator,直接用它):publicinterfaceIteratorT{booleanhasNext();// 还有下一个吗Tnext();// 取出下一个,并前进}第二步,集合实现Iterable,提供自己的迭代器:publicclassOrderListimplementsIterableOrder{privateOrder[]ordersnewOrder[100];privateintsize0;publicvoidadd(Ordero){orders[size]o;}// 返回一个迭代器,把怎么遍历封装进去publicIteratorOrderiterator(){returnnewIteratorOrder(){privateintcursor0;// 遍历游标,是迭代器自己的状态publicbooleanhasNext(){returncursorsize;}publicOrdernext(){returnorders[cursor];}};}}第三步,使用方只通过迭代器遍历,不碰内部结构:OrderListlist...;IteratorOrderitlist.iterator();while(it.hasNext()){Orderoit.next();// 只问迭代器要下一个,不知道内部是数组还是链表// ...}对比第一节,升级点非常清晰:使用方再也不知道OrderList内部是什么结构,它只会问迭代器还有吗、下一个是谁;哪天内部从数组改成链表,只需重写iterator()里迭代器的实现(改成顺指针走),所有使用方代码一个字都不用改。遍历逻辑被彻底封装进了迭代器,和集合内部结构解耦了。用一张图看这个遍历分离的结构最清楚:图里最该记住的,是那个夹在使用方和集合内部结构之间的迭代器——它像一个游标,记着当前遍历到哪了,对外只暴露hasNext()/next()两个简单动作。这里还有个额外好处:遍历状态(游标)存在迭代器里,而不是集合里,所以同一个集合可以同时有多个迭代器各自独立遍历,互不干扰。三、Java 内置的迭代器:for-each 的真相前面说迭代器是你用得最多的模式,因为Java 把它做成了语言级的基础设施。核心是两个接口:IterableT:表示我是可迭代的,只有一个方法iterator()返回一个迭代器。所有集合类(List、Set、Queue……)都实现了它。IteratorT:迭代器本身,hasNext()/next()(还有个remove())。而你天天写的增强 for 循环(for-each),本质就是迭代器的语法糖。这段代码:for(Ordero:orders){// 增强 forprocess(o);}编译器会把它自动翻译成基于迭代器的循环:IteratorOrderitorders.iterator();// 编译器帮你调 iterator()while(it.hasNext()){Orderoit.next();process(o);}这就是为什么for-each能遍历所有集合——只要一个类实现了Iterable,它就能被for-each遍历。反过来,如果你自己写的类(像第二节的OrderList)实现了Iterable,那它也能直接用for-each:OrderListlist...;for(Ordero:list){// 我们的自定义集合,也能用 for-each 了!// ...}这正是实现Iterable的最大好处:让你的自定义集合无缝融入 Java 的for-each语法。这也是迭代器模式统一遍历方式价值的最好体现——不管什么集合、内部什么结构,一律for (x : collection),写法完全一致。四、一个天天遇到的机制:fail-fast讲迭代器,绕不开一个几乎每个 Java 开发者都踩过的坑——在 for-each 遍历集合时修改它,会抛ConcurrentModificationException:ListOrderordersnewArrayList(...);for(Ordero:orders){if(o.isCancelled()){orders.remove(o);// ✗ 遍历中直接删,抛 ConcurrentModificationException}}为什么会抛异常?这就是迭代器的fail-fast(快速失败)机制。原理是:集合内部维护一个修改次数计数器modCount,每次增删都会让它 1。迭代器创建时会记下当时的modCount,之后每次next()都检查集合的modCount有没有变——如果你在遍历过程中通过集合本身增删了元素,modCount就变了,迭代器一检测到不一致,立刻抛异常。fail-fast 的意图是快速暴露问题:遍历时结构被意外修改,往往是并发 bug 或逻辑错误,与其让遍历得到一个诡异错乱的结果,不如立刻抛异常、当场失败,让你早点发现。这是一种保护机制。那正确的边遍历边删除怎么做?用迭代器自己的remove()方法——因为迭代器的remove()会同步更新它记录的modCount,不会触发 fail-fast:IteratorOrderitorders.iterator();while(it.hasNext()){Orderoit.next();if(o.isCancelled()){it.remove();// ✓ 用迭代器的 remove,安全}}// 或者更简洁:orders.removeIf(Order::isCancelled); // Java 8理解了迭代器的游标和modCount机制,这个天天遇到的坑就一清二楚了——它不是 Java 的 bug,而是迭代器有意的自我保护。五、什么时候用迭代器模式适合用迭代器模式的信号:你写了一个自定义的集合/容器,希望它能被统一地、优雅地遍历(尤其想支持for-each)——实现Iterable即可;你想在不暴露集合内部结构的前提下提供遍历能力;你需要对同一集合支持多种遍历方式(正序、倒序、过滤),可以提供多个不同的迭代器。不必特意用的信号:你用的就是 Java 标准集合(List/Set/Map)——那你已经在用迭代器了(for-each就是),不需要额外做什么;就一个简单数组、按下标遍历完全够用——那直接 for 循环,不必套迭代器。关于这个模式,最实在的一句话是:你几乎不需要从头实现迭代器模式,因为 Java 已经内置了。你要做的,通常只是让自定义集合实现Iterable接口,从而免费获得统一遍历和for-each支持。真正需要手写迭代器逻辑的场景不多(比如遍历一个特殊的树结构、或实现惰性遍历),但理解它的原理,能让你彻底搞懂for-each和fail-fast,不再对它们感到神秘。小结。迭代器模式提供一种统一的方式遍历集合,而不暴露其内部结构——把怎么遍历从集合中分离,封装进一个持有游标、只暴露hasNext()/next()的迭代器对象。它是 Java 里最隐形的模式,因为已被做成语言级基础设施:Iterable/Iterator两个接口 for-each语法糖,让你能用完全一致的写法遍历任何集合,不管内部是数组、链表还是哈希表。你天天遇到的for-each就是迭代器的语法糖,ConcurrentModificationException就是迭代器的 fail-fast 自我保护(正确做法是用迭代器的remove()或removeIf)。实际开发中你很少从头实现它,通常只需让自定义集合实现Iterable就能免费享受这一切。下一篇我们讲备忘录模式——它和命令模式的撤销相呼应,专门负责给对象的状态拍快照,让对象能随时回滚到某个历史状态,典型如订单编辑时的存草稿、恢复草稿。