尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

像翻书一样遍历数据:迭代器模式详解与实战

发布时间:2026/9/29 20:15:25

资讯中心
01
ARTICLE

像翻书一样遍历数据:迭代器模式详解与实战

像翻书一样遍历数据:迭代器模式详解与实战
写代码这些年我越来越觉得很多设计模式并没有想象中那么玄乎它就是把日常处理事务的自然逻辑提炼成了规矩。就拿标题里这个“像翻书一样遍历数据”来说——你读一本书从来不会把整本书倒出来一页一页摆满桌子只会按顺序从第1页往后翻翻到哪就是哪再用书签夹住当前位置。迭代器模式Iterator Pattern干的就是这件小事把“遍历一个数据集合”这个动作抽象成“翻一页看一页”让调用方不需要关心背后的数据到底是数组、链表、树还是数据库游标也不需要知道数据集总共多大、元素存在哪个内存角落。只要拿一个迭代器hasNext()问一句“还有没有下一页”next()翻过去取出内容就完事了。这篇文章要讲清楚三件事迭代器模式为什么值得学、怎么从零手写一个可用的迭代器以及真实项目中遍历数据时最容易踩的坑。它适合刚接触设计模式、想把for循环写得更有章法的开发者也适合准备面试时总被追问“手写迭代器说一下快速失败机制”的老手。我会把思路、代码、坑位都一并抖出来争取看完就能直接上手用。1. 迭代器模式的设计思路与核心角色拆解1.1 没有迭代器时我们经历过的痛苦先回忆一个很常见的场景你负责维护一个图书管理系统内部的数据结构一开始用的是ArrayList于是你满脑子都是for i加get(i)。某天需求变了要换成LinkedList或者要换成一个按书名排序的自定义树结构这时候你会发现所有依赖“按索引访问”的代码全部要跟着改。这就是没有迭代器时的第一个痛苦调用方和数据结构强耦合。业务代码被迫知道容器内部用的是数组还是链表一旦容器换实现业务代码也要跟着返工。第二个痛苦是遍历逻辑散落得到处都是。每个业务方法里都写着自己的循环体循环的判断条件、游标的进退规则各不相同。有人从0开始有人从1开始有人用while有人用递归时间一长代码里全是风格迥异的遍历方式别人接手的时候只能一头雾水地猜。第三个痛苦是封装被破坏。如果你给外部提供的是一个public ListBook getBooks()方法等于把整个内部存储全部交了出去。调用方可以随意增删改容器自己完全控制不住数据完整性。可如果不提供内部结构调用方又没有办法拿到数据最后只能往类里堆一大堆getBookAt(int index)、getSize()之类的访问方法类越来越臃肿职责越来越混乱。1.2 四个核心角色以及它们各自的职责迭代器模式把这个问题拆得非常干净一共只有四个角色Iterator迭代器接口约定两个基本能力——hasNext()判断是否还有下一个元素next()返回下一个元素。有些语言还会加一个remove()但我会在后面的章节详细说这个remove()其实是个麻烦制造者。ConcreteIterator具体迭代器真正记录“当前翻到第几页”的地方。它的内部通常持有一个游标index如果是链表结构就可能持有当前节点引用。Aggregate聚合接口声明一个返回迭代器的方法通常叫iterator()或者createIterator()。它代表“这本书可以翻”。ConcreteAggregate具体聚合类真正存储数据的地方比如书架、列表、树。它负责创建出一个对自己的数据集合有感知的具体迭代器。这四个角色在翻书类比里对应得明明白白书就是聚合类书签就是迭代器里的游标状态翻页的动作就是next()判断还有没有下一页就是hasNext()。1.3 为什么“像翻书”这个类比如此贴切我特别喜欢标题里这个比喻因为它一下抓住了迭代器模式的三个本质特征。第一是顺序访问。翻书只能一页一页地看你不需要荡气回肠地跳着读迭代器也不承诺随机访问。它故意不给你get(103)这种能力逼着你按顺序走完整个序列。这个限制恰恰是优点调用方逻辑简单迭代器实现也简单。第二是状态可以共存。同一本书你完全可以同时夹两个书签一个在序言一个在附录。迭代器模式也一样同一个聚合对象可以同时存在多个迭代器各自推进互不干扰。这个特性在写嵌套循环、分页加载时极其有用。第三是内部表示完全隐藏。你翻一本纸质书根本不需要知道纸张是怎么造的墨水是喷上去的还是印上去的。迭代器把“如何存储”“如何定位下一个元素”这些细节统统藏到了具体迭代器内部调用方只依赖稳定的next()接口。再说深一层这个类比还能解释“为什么把遍历算法从容器里拆出来”的设计取舍。如果树的遍历方法直接写在树类里前序、中序、后序、层序各写一个方法树类很快会膨胀成一个无所不包的巨类。而用迭代器模式每种遍历方式独立成一个类按需创建想用哪种就 new 哪种容器和算法的职责一分为二改起来互不影响。2. 从零手写一个迭代器完整实操过程2.1 场景设定一个可以翻的书架理论说完了直接上手写代码。我们构建一个最小的案例一个BookShelf书架内部用ArrayList存书外部可以通过迭代器按顺序遍历每一本书。这个案例虽然简单但它能完整展示聚合类和迭代器类如何配合。第一步定义迭代器接口。在实际项目中如果你用的是Java直接用java.util.Iterator就行但为了看清原理我们先用自建的接口public interface IteratorE { boolean hasNext(); E next(); }第二步定义可迭代的聚合接口public interface AggregateE { IteratorE iterator(); }2.2 写一个ConcreteAggregateBookShelfBookShelf干两件事负责存放书负责创建迭代器。它内部到底用ArrayList还是数组外部完全不需要知道。public class BookShelf implements AggregateBook { private ListBook books new ArrayList(); public void addBook(Book book) { books.add(book); } public int getSize() { return books.size(); } public Book getBookAt(int index) { return books.get(index); } Override public IteratorBook iterator() { return new BookShelfIterator(this); } }注意看我在这里保留了getSize()和getBookAt(int index)这两个方法是用给迭代器用的。它们是聚合类开放的内部访问点但调用方不需要直接碰它们因为迭代器会屏蔽掉这些细节。2.3 写一个ConcreteIteratorBookShelfIterator这是整个模式最关键的部分游标状态就藏在这里public class BookShelfIterator implements IteratorBook { private BookShelf bookShelf; private int index; public BookShelfIterator(BookShelf bookShelf) { this.bookShelf bookShelf; this.index 0; } Override public boolean hasNext() { return index bookShelf.getSize(); } Override public Book next() { return bookShelf.getBookAt(index); } }这里有几个容易被忽略的细节index从0开始hasNext()判断的是“当前游标是否还没有越过最后一位元素”。注意next()返回的是bookShelf.getBookAt(index)也就是说先取出当前游标位置的元素再把游标往后移动一位。这个“先取后移”的顺序已经成了迭代器的事实标准。如果调用方不检查hasNext()直接连续调用next()当游标越界时getBookAt(index)会抛出越界异常。所以在真正的迭代器实现里一般会先做一次显式检查发现没有下一个元素就直接抛出NoSuchElementException而不是等到getBookAt那里抛一个语义不清晰的异常。这个习惯值得养成。2.4 为什么“当前读到哪”不能放在聚合类里很多初学者会疑惑既然index只是记录读到哪一页把它放在BookShelf里不行吗反正书架总共也就一个。答案是不行。想象一下如果两个业务逻辑同时遍历同一个书架一个正在读前50本统计平均价格另一个想从第10本开始刷新封面缓存。如果把游标放在聚合类里第二个遍历一开始就把第一个遍历的位置顶掉了第一个循环瞬间错乱。这就是“书签必须由读者自己带着”的道理。聚合类就像一本书书本身不应该关心有多少人正在读、各自读到哪页了。每个迭代器自己保存一份独立的index多路遍历才能并行不悖。这个设计点在整个模式里价值极高值得反复体会。2.5 用Python再写一遍感受语言层面的差异我们可以用Python实现同一个书架对照理解class BookShelfIterator: def __init__(self, shelf): self._shelf shelf self._index 0 def __next__(self): if self._index len(self._shelf._books): raise StopIteration book self._shelf._books[self._index] self._index 1 return book class BookShelf: def __init__(self): self._books [] def add_book(self, book): self._books.append(book) def __iter__(self): return BookShelfIterator(self)调用方式shelf BookShelf() shelf.add_book(设计模式) shelf.add_book(代码大全) for book in shelf: print(book)注意Python这里的细微差别Python的for循环会自动捕获StopIteration异常并停止遍历迭代器只需要实现__next__即可。这个异常本质上是“没有下一页了”的显式信号。对比Java版本两种语言一个用返回值判断、一个用异常终止但模式骨架完全一致这恰好说明迭代器模式的语言无关性。3. 进阶实战语言内置迭代器与复杂场景应用3.1 Java的Iterable与增强for循环实际项目里很少有人会自己去实现java.util.Iterator接口的完整语义大部分时候只需要让类实现IterableT接口就能直接用增强forpublic class BookShelf implements IterableBook { // 省略存储细节 Override public IteratorBook iterator() { return new BookShelfIterator(this); } }然后调用方这样写for (Book book : bookShelf) { System.out.println(book.getName()); }编译器会把这段语法糖展开成IteratorBook it bookShelf.iterator(); while (it.hasNext()) { Book book it.next(); System.out.println(book.getName()); }也就是说增强for循环不过是迭代器模式的语法封装。理解了底层原理你在阅读反编译代码或者排查ConcurrentModificationException时就能一眼看出循环展开后的真实逻辑。3.2 Python生成器编译器帮你写好了迭代器Python里有一个更偷懒的大杀器生成器generator。它让你用几乎“写普通函数”的姿势完成迭代器def book_gen(shelf): for book in shelf._books: yield book for book in book_gen(shelf): print(book)yield关键字背后藏着一个完整的状态机函数每次执行到yield就暂停把当前局部变量、程序计数器全部保存起来下一次调用next()时从暂停点继续执行。你不需要手动管理index也不需要手动判断边界这些杂事全部交给了编译器生成的隐藏状态机。这个特性和“看书先夹书签”的道理一模一样你在某一页夹一个书签回头再看时从这一页接着读不用重新从第1页开始找。生成器的yield就是那个书签。3.3 C#与JavaScript的迭代器对比C#的迭代器实现更是把“惰性求值”玩到了极致。用yield return写的迭代器本质上被编译器转换成了一个实现了IEnumeratorT的状态机类这个类的MoveNext()方法对应hasNext()Current属性对应next()的返回值。C#里的LINQ之所以能实现链式惰性查询底层靠的就是这套迭代器机制。JavaScript则提供了显式的迭代器协议。一个对象只要部署了[Symbol.iterator]方法并返回next函数就能被for...of循环消费const bookShelf { books: [ { title: 设计模式 }, { title: 代码大全 } ], [Symbol.iterator]() { let index 0; return { next: () { if (index this.books.length) { return { done: true }; } return { done: false, value: this.books[index] }; } }; } }; for (const book of bookShelf) { console.log(book.title); }JavaScript这里的done字段和Python的StopIteration、Java的hasNext()本质上是同一件事的三种说法告诉消费者“数据流已经走完了”。我把这几个语言的差异整理成一张表方便对照语言迭代器接口/协议循环语法惰性求值生成器支持JavaIteratorT/IterableTfor (T t : coll)需自行设计无原生yieldPython__iter__/__next__for x in obj天然支持yieldC#IEnumeratorT/IEnumerableTforeach (T t in coll)天然支持yield returnJavaScript[Symbol.iterator]协议for...of天然支持function*yield看完这张表你会发现主流语言已经把迭代器模式内化成了语法一级的支持。但语法支持并不代表你可以不学原理因为一旦遇到性能问题、并发修改或者需要自己设计一种全新的遍历顺序时你还是得回到模式本身去理解它。3.4 迭代器在链表和树形结构上的应用讲完了语言层面的差异再讲两个最常见的复杂数据结构上的迭代器实现。链表是迭代器最天然的适配对象。链表本来就不支持随机访问你要找第i个节点只能从头节点逐个next过去。迭代器内部直接保存当前节点引用每次next()执行current current.next时间复杂度降到O(1)。很多刚写完链表就想用for i遍历的新手会撞得头破血流其实就是还没适应这个道理链表天然是“迭代器友好”的绝不是“索引友好”的。树形结构更有意思。前序遍历、中序遍历、后序遍历、层序遍历本质上是四套完全不同的遍历策略。如果把它们全部堆在树类里树类会变得又臭又长如果用迭代器模式每种遍历写成独立迭代器按需取用。以层序遍历为例迭代器内部持有一个队列next()时从队头取节点然后把它的左右孩子依次入队外部调用方完全感知不到队列的存在它只需要不停地next()就能一层一层地把树读完。这是迭代器封装复杂状态的一个绝佳案例。另外提醒一句数据库游标、文件读取流、分页查询结果集本质上都是迭代器思想。它们全都遵循同一个原则——一次只取一条按需拉取边读边丢从而避免一次性把大量数据灌进内存。理解了迭代器模式你会觉得这些技术背后的设计逻辑全都串起来了。4. 常见问题与排查技巧实录4.1 在Python的list遍历过程中删除元素问题出在哪热搜里有一条“pythonlist遍历删除”这几乎是每个Python开发者都踩过的坑。很多人写过这样一段代码lst [1, 2, 3, 4, 5] for item in lst: if item % 2 0: lst.remove(item)执行完你会发现列表压根没有按照预期删干净。原因在于for循环底层用的是迭代器迭代器内部维护着一个基于索引的游标。每删除一个元素后面的所有元素集体前移而游标还会继续往后走于是紧跟在被删除元素后面的那一个就被直接跳过了。老老实实看一遍执行细节第2个元素被删除后原来的第3个元素变成了新的第2个元素但游标已经进到第3个位置这个新元素的命运就是被跳过去。解决方案有几种反向遍历for item in reversed(lst)从尾到头删除前面的元素不会受影响用列表推导式生成新列表lst [x for x in lst if x % 2 ! 0]然后直接替换引用用while循环手动控制索引如果确实需要边遍历边删除且要求高效借助collections.deque等专门的数据结构。4.2 迭代器失效集合变了迭代器还拿着旧状态Java用户对这个异常应该非常熟悉ConcurrentModificationException。当你用迭代器遍历一个ArrayList时如果另一个线程或者同一个循环体里直接调用了list.add()或list.remove()迭代器在下一次操作时会立刻抛出这个异常。这是因为ArrayList内部维护了一个modCount字段记录结构被修改的次数。迭代器创建时会保存当时的modCount每次next()之前都会检查一遍当前值是否仍然相等不等就直接快速失败。这个机制叫“快速失败”fail-fast它的设计意图是与其让你在数据错乱的环境里继续跑出错误结果不如立刻告诉你“你对这个集合的结构做了非法修改”。碰到这种情况解决办法通常是四个方向遍历时只读不写把要删除的元素先收集到一个临时列表遍历结束后统一删除使用CopyOnWriteArrayList它的迭代器是弱一致性的允许在遍历期间修改结构显式加锁保证修改和遍历串行化改用removeIf()这类容器自带的条件删除方法内部会正确处理迭代器状态。4.3 迭代器接口上该不该有remove方法这算得上是一个设计争议点。java.util.Iterator本身提供了remove()方法但它的语义限制非常严格只能删除刚被next()返回的那个元素而且在调用remove()之前必须先调用过一次next()。很多人根本不用它因为它既容易写错又容易把容器结构搞乱。我的建议是自己设计迭代器接口时尽量不要把remove()放进去。原因很简单迭代器模式的定位是“遍历”不是“修改”。删除操作应该由聚合类本体提供明确的方法比如removeById而不是通过迭代器的后门摸进去。如果实在需要边遍历边删现代语言提供的removeIf、列表推导式、Collectors.toList等方案都更安全、语义更清晰。4.4 自定义容器忘了实现迭代器接口会发生什么这种问题在现实项目里太常见了。你写了一个OrderCollection觉得内部用一个List存订单就够了于是直接把getOrders()方法暴露出去让外部自己遍历。一开始还行直到某个调用方为了性能把List换成了自定义的链表结构你的getOrders()被迫跟着改动所有调用方的代码都要重新编译、重新测试。反过来如果一个自定义容器从一开始就实现了Iterable接口那么不管是内部存数组、链表、树还是数据库游标调用方永远只需要写for (Order order : collection)这一行。这个投资回报率极高所以我强烈建议凡是“装着一堆对象”的自定义类第一步就要问自己“它需要被遍历吗”。需要就先实现迭代器再写其他业务逻辑。4.5 几个容易让人当场懵掉的细节整理几个我在代码评审里反复见到的问题hasNext()不会移动游标next()才会。所以如果你在循环里不小心调用了两次next()就会跳过元素。这个错误非常隐蔽尤其在处理树结构时表现成“莫名其妙的漏数据”。每个迭代器的生命周期只属于一次遍历。遍历到末尾之后这个迭代器就废了想重新遍历要再调一次iterator()拿新迭代器。这就像书签抽出来之后再想从开头看就得重新夹一个。迭代器持有了大对象的引用用完后不及时释放。遍历完成就把它丢掉别一直攥在手里否则可能造成不必要的内存驻留。嵌套遍历同一个集合时内层循环尽量使用新迭代器。如果外层迭代器还在使用就被内层操作修改了集合结构那又是一颗定时炸弹。我自己的习惯是写任何自定义集合类时永远先把迭代器实现放进去再写业务方法。这个顺序能倒逼我思考清楚这个类的边界和职责——它对外暴露什么、隐藏什么、允许多少种遍历方式都一目了然。至于遇到特别复杂的遍历场景比如树、图、嵌套结构那就老老实实在纸上把状态机画清楚再动代码先设计“书签夹在哪”再设计“怎么翻页”一次成型后面省下的调试功夫远比你预想的多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。