1. 从一次线上遍历事故说起:Java 迭代器模式到底解决什么问题
很多人第一次接触 Java 迭代器模式(Iterator Pattern),是在for (String s : list)这种语法糖里。编译器帮你把增强 for 循环翻译成Iterator的hasNext()和next()调用,你几乎感觉不到它的存在。可一旦你开始写自己的容器类,或者需要在遍历过程中安全地删除元素,迭代器模式就从「背景板」变成了必须正面处理的核心设计。
我遇到过一个很典型的问题:团队里有人写了一个固定容量的环形缓冲区,对外只暴露get(int index)和size()。调用方为了遍历,直接写for (int i = 0; i < buf.size(); i++)。看起来没问题,直到有人需要在遍历时按条件移除元素,于是出现了下标错位、元素被跳过、甚至IndexOutOfBoundsException。根因不是代码写得差,而是这个容器没有提供统一的遍历抽象——它把内部存储结构(数组下标)暴露给了调用方,调用方就得自己维护遍历状态,一旦容器内部结构调整,所有调用点都要跟着改。
迭代器模式要解决的就是这件事:把「遍历状态」从容器里剥离出来,交给一个独立的迭代器对象持有。容器只负责存数据,迭代器负责记录「我现在走到哪了」。这样带来三个直接好处。第一,调用方不需要知道容器底层是数组、链表还是树,只需要拿到一个Iterator,就能用同一套hasNext()/next()逻辑遍历。第二,同一个容器可以同时开多个迭代器,各自维护各自的游标,互不干扰。第三,遍历算法可以替换,正序、倒序、按条件过滤,都可以是不同的迭代器实现,而容器本身不用动。
在 Java 集合框架里,这套思想已经落地得很彻底。Collection接口继承了Iterable,Iterable定义了iterator()方法,List、Set、Queue各自提供具体迭代器。ArrayList用内部类Itr实现,LinkedList用ListItr,HashMap用HashIterator。它们都遵循同一个契约:hasNext()判断是否还有元素,next()返回下一个元素并推进游标,remove()删除最近一次next()返回的元素。
但真实项目里,光会用 JDK 自带的迭代器还不够。你可能会遇到这些场景:自定义数据结构需要支持for-each;需要在遍历时按业务规则过滤;需要倒序遍历一个只提供正序迭代器的容器;或者需要把多个容器的遍历统一成一个接口。这些都需要你自己实现Iterable和Iterator。
这篇内容就围绕「Java 迭代器模式实战」展开。我会先讲清楚Iterator接口的契约和hasNext/next/remove的行为边界,然后带你写一个自定义Iterable容器,给出可复制的配置片段,再演示在 IDE 里验证遍历行为的完整步骤。过程中会结合 TaoToken 的统一 Key 和 API 通道,让 AI 辅助生成迭代器代码、排查遍历异常。如果你正在写自己的集合类,或者被ConcurrentModificationException折腾过,这篇应该能帮你把迭代器模式真正落地。
2. 前置准备:用 TaoToken 统一 Key 接入 AI 辅助生成迭代器代码
在开始写自定义迭代器之前,先把手头的 AI 编码辅助通道理顺。我试过在多个 IDE 插件、多个模型之间来回切换,最烦的就是每个工具都要单独配 Key、单独记 Base URL,改一个模型要动好几处配置。TaoToken 的思路是提供一个统一的 API 通道,你只需要维护一个 Key 和一个 Base URL,就能在模型对话、Coding Plan、API Keys 等不同入口之间复用。
先明确几个地址,后面配置会反复用到。官网入口是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,API 基础地址是https://taotoken.net/api(这个不加 UTM 参数)。如果你要管理 Key,去 API Keys 页面;要验证模型是否通,用模型对话页面;如果是长期编码或 Agent 场景,看 Coding Plan。
这里要强调一点:TaoToken 在这里的角色是「AI 辅助编码的通道」,不是替代你的 IDE 或编译器。迭代器代码最终还是要落到你的 Java 工程里,由 JDK 编译执行。AI 帮你做的是生成模板、解释报错、补全边界判断,而不是替你运行程序。
配置的核心是三件套:Base URL、API Key、Model ID。无论你用的是 Cline、Claude Code 还是其他支持 OpenAI 兼容协议的工具,这三项都是必须的。Base URL 填https://taotoken.net/api,API Key 从控制台生成,Model ID 根据你实际使用的模型填写。下面给一个通用的 JSON 配置片段,你可以直接复制到支持 OpenAI 兼容格式的客户端配置里:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的TaoTokenKey", "model": "你的ModelID", "temperature": 0.2, "max_tokens": 4096 }如果你用的是 Claude Code 这类工具,配置方式会略有不同,通常需要设置环境变量或写入 settings 文件。核心还是那三件套,只是字段名不一样。比如在 Claude Code 的配置里,你会看到ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY这样的字段,Base URL 同样指向 TaoToken 的 API 地址,Key 用你生成的统一 Key。
为什么要用统一 Key?因为迭代器模式的实战往往涉及多轮对话:先让 AI 生成一个基础Iterator实现,再让它补上remove()的边界处理,然后针对ConcurrentModificationException做排查。如果每换一个工具就要重新配一次,上下文就断了。统一 Key 让你在模型对话、Coding Plan、API 调用之间共享同一套凭证,切换成本低很多。
还有一点值得提醒:生成迭代器代码时,建议把temperature调低,比如 0.2 左右。迭代器涉及游标状态、边界判断、并发修改检测,这些逻辑对准确性要求高,温度太高容易生成看似合理但边界错误的代码。低温度下模型更倾向于输出确定性的、符合契约的实现。
准备好 Key 和 Base URL 之后,你就可以在 IDE 里让 AI 帮你生成Iterator骨架了。下一节我会给出完整的可复制配置和自定义迭代器实现,包括hasNext、next、remove三个方法的行为约定。
3. 可复制配置:自定义 Iterable 与 Iterator 完整实现
这一节给出可以直接复制到 Java 工程里的迭代器实现。我会先写一个固定容量的SimpleList容器,实现Iterable<String>,然后提供一个内部类迭代器,完整实现hasNext()、next()、remove()。这个实现会遵循 JDK 的迭代器契约,包括remove()只能在next()之后调用一次、遍历中修改容器要抛ConcurrentModificationException。
先看容器接口和实现。为了让for-each能用,容器必须实现Iterable<T>,并返回Iterator<T>:
import java.util.Iterator; import java.util.NoSuchElementException; import java.util.ConcurrentModificationException; public class SimpleList<T> implements Iterable<T> { private Object[] elements; private int size; private int modCount; public SimpleList(int capacity) { if (capacity <= 0) { throw new IllegalArgumentException("capacity must be positive"); } this.elements = new Object[capacity]; this.size = 0; this.modCount = 0; } public void add(T element) { if (size >= elements.length) { throw new IllegalStateException("container is full"); } elements[size++] = element; modCount++; } public int size() { return size; } @SuppressWarnings("unchecked") public T get(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("index: " + index); } return (T) elements[index]; } @Override public Iterator<T> iterator() { return new SimpleIterator(); } private class SimpleIterator implements Iterator<T> { private int cursor; private int lastRet; private int expectedModCount; SimpleIterator() { this.cursor = 0; this.lastRet = -1; this.expectedModCount = modCount; } @Override public boolean hasNext() { return cursor < size; } @Override @SuppressWarnings("unchecked") public T next() { checkForComodification(); if (cursor >= size) { throw new NoSuchElementException(); } T value = (T) elements[cursor]; lastRet = cursor; cursor++; return value; } @Override public void remove() { if (lastRet < 0) { throw new IllegalStateException("remove() called before next()"); } checkForComodification(); System.arraycopy(elements, lastRet + 1, elements, lastRet, size - lastRet - 1); elements[--size] = null; cursor = lastRet; lastRet = -1; modCount++; expectedModCount = modCount; } private void checkForComodification() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } } }这段代码有几个关键点需要解释。cursor是游标,指向下一个要返回的元素下标;lastRet记录最近一次next()返回的元素下标,初始为 -1,表示还没调用过next()。remove()的第一件事就是检查lastRet < 0,如果是,说明调用方在next()之前就调了remove(),直接抛IllegalStateException。这是 JDK 迭代器的标准行为,很多自己写的迭代器会漏掉这个检查。
expectedModCount是并发修改检测的核心。容器每次结构性修改(add、remove)都会让modCount自增。迭代器创建时记录当时的modCount,每次next()和remove()前都对比一次。如果发现不一致,说明迭代器遍历期间容器被外部修改了,抛ConcurrentModificationException。这就是为什么你在遍历ArrayList时调用list.remove()会报错,而调用iterator.remove()不会——后者会同步更新expectedModCount。
remove()里的System.arraycopy做元素前移,然后size--,把最后一个位置置空帮助 GC。注意cursor = lastRet这一步:删除当前元素后,后面的元素前移,游标要回退到被删位置,这样下一次next()才能正确返回前移过来的元素。lastRet重置为 -1,保证remove()不能连续调用两次。
如果你用 AI 辅助生成这段代码,可以把上面的实现作为参考,让模型帮你补全注释或生成单元测试。配置上,把 Base URL 设为https://taotoken.net/api,Key 用你的统一 Key,Model ID 按实际填写。在模型对话里贴出你的容器代码,问「这个迭代器的 remove 在连续调用两次时会怎样」,模型会基于契约给出分析。
下面再给一个倒序迭代器的实现,展示「同一容器多种遍历方式」这个迭代器模式的核心优势:
public Iterator<T> reverseIterator() { return new Iterator<T>() { private int cursor = size - 1; private int lastRet = -1; private int expectedModCount = modCount; @Override public boolean hasNext() { return cursor >= 0; } @Override @SuppressWarnings("unchecked") public T next() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } if (cursor < 0) { throw new NoSuchElementException(); } T value = (T) elements[cursor]; lastRet = cursor; cursor--; return value; } @Override public void remove() { if (lastRet < 0) { throw new IllegalStateException(); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } System.arraycopy(elements, lastRet + 1, elements, lastRet, size - lastRet - 1); elements[--size] = null; if (lastRet < cursor) { cursor--; } lastRet = -1; modCount++; expectedModCount = modCount; } }; }倒序迭代器的remove()里,游标调整逻辑和正序不同:如果被删元素在游标左侧,游标要减一。这个细节很容易写错,建议用单元测试覆盖。
4. 验证请求与成功结果:在 IDE 中跑通 hasNext/next/remove 行为
代码写完了,接下来要在 IDE 里验证行为是否符合预期。我习惯用 JUnit 写一组测试,覆盖正常遍历、边界情况、并发修改、remove 语义这四类场景。下面给出完整的测试代码,你可以直接复制到src/test/java下运行。
import org.junit.jupiter.api.Test; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.ConcurrentModificationException; import static org.junit.jupiter.api.Assertions.*; public class SimpleListTest { @Test void shouldIterateAllElementsInOrder() { SimpleList<String> list = new SimpleList<>(4); list.add("aa"); list.add("bb"); list.add("cc"); list.add("dd"); StringBuilder sb = new StringBuilder(); for (String s : list) { sb.append(s).append(" "); } assertEquals("aa bb cc dd ", sb.toString()); } @Test void shouldThrowWhenNextBeyondEnd() { SimpleList<String> list = new SimpleList<>(2); list.add("x"); Iterator<String> it = list.iterator(); assertTrue(it.hasNext()); assertEquals("x", it.next()); assertFalse(it.hasNext()); assertThrows(NoSuchElementException.class, it::next); } @Test void shouldRemoveCurrentElement() { SimpleList<String> list = new SimpleList<>(4); list.add("aa"); list.add("bb"); list.add("cc"); list.add("dd"); Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if ("bb".equals(s)) { it.remove(); } } assertEquals(3, list.size()); assertEquals("aa", list.get(0)); assertEquals("cc", list.get(1)); assertEquals("dd", list.get(2)); } @Test void shouldThrowWhenRemoveBeforeNext() { SimpleList<String> list = new SimpleList<>(2); list.add("a"); Iterator<String> it = list.iterator(); assertThrows(IllegalStateException.class, it::remove); } @Test void shouldThrowWhenRemoveTwice() { SimpleList<String> list = new SimpleList<>(2); list.add("a"); list.add("b"); Iterator<String> it = list.iterator(); it.next(); it.remove(); assertThrows(IllegalStateException.class, it::remove); } @Test void shouldThrowOnConcurrentModification() { SimpleList<String> list = new SimpleList<>(4); list.add("aa"); list.add("bb"); list.add("cc"); Iterator<String> it = list.iterator(); it.next(); list.add("dd"); assertThrows(ConcurrentModificationException.class, it::next); } }跑完这组测试,你应该看到全部通过。如果某个用例失败,对照报错信息定位。比如shouldRemoveCurrentElement失败,大概率是remove()里游标调整写错了;shouldThrowOnConcurrentModification失败,说明expectedModCount没有在add时更新,或者checkForComodification没被调用。
除了单元测试,我还建议在main方法里手动跑一遍,观察控制台输出。下面这段代码模拟了「遍历中删除偶数位置元素」的场景:
public class IteratorDemo { public static void main(String[] args) { SimpleList<String> list = new SimpleList<>(6); list.add("aa"); list.add("bb"); list.add("cc"); list.add("dd"); list.add("ee"); list.add("ff"); System.out.println("before: size=" + list.size()); Iterator<String> it = list.iterator(); int index = 0; while (it.hasNext()) { String s = it.next(); if (index % 2 == 1) { it.remove(); System.out.println("removed: " + s); } index++; } System.out.println("after: size=" + list.size()); for (String s : list) { System.out.println("remaining: " + s); } } }预期输出是:before: size=6,依次removed: bb、removed: dd、removed: ff,最后after: size=3,剩余aa、cc、ee。如果你看到ConcurrentModificationException,检查remove()里是否更新了expectedModCount。
验证过程中如果遇到报错,可以把异常栈贴到模型对话里,让 AI 帮你分析。TaoToken 的模型对话入口支持直接粘贴代码和报错,Base URL 用https://taotoken.net/api,Key 用统一 Key。对于迭代器这类契约性强的代码,AI 在解释ConcurrentModificationException触发条件时通常比较准确。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 对照
这一节整理实战中容易遇到的几类报错,分两部分:一部分是迭代器代码本身的逻辑错误,另一部分是接入 TaoToken 时可能出现的配置错误。两类问题经常混在一起,因为你在 IDE 里让 AI 生成代码时,如果通道不通,会先看到接入报错,误以为是代码问题。
先看迭代器代码的常见错误。第一类是NoSuchElementException,通常是在hasNext()返回 false 之后还调next()。检查你的循环条件,确保next()只在hasNext()为 true 时调用。第二类是IllegalStateException: remove() called before next(),说明remove()前没有调用next(),或者连续调了两次remove()。第三类是ConcurrentModificationException,说明遍历期间容器被外部修改,或者你的remove()没有同步expectedModCount。第四类是元素被跳过或重复,通常是remove()后游标没有正确回退。
再看接入侧的报错。如果你在配置 AI 辅助工具时看到401 Unauthorized,先检查 API Key 是否正确、是否过期、有没有多余空格。Key 从 API Keys 页面生成,复制时注意不要带上换行。如果看到local proxy failed或类似的连接错误,检查 Base URL 是否写成了https://taotoken.net/api,注意结尾不要多加斜杠,也不要误填成官网首页地址。如果看到reading choices相关的解析错误,通常是响应格式和客户端预期不一致,检查 Model ID 是否填写正确,以及客户端是否按 OpenAI 兼容格式解析。
下面用表格对照几类典型报错和排查方向:
| 报错关键词 | 可能原因 | 排查动作 |
|---|---|---|
| 401 Unauthorized | Key 错误或过期 | 重新生成 Key,检查空格和换行 |
| local proxy failed | Base URL 配置错误 | 确认填https://taotoken.net/api |
| reading choices | Model ID 不匹配 | 核对 Model ID,确认客户端协议 |
| OAuth 相关报错 | 认证方式选错 | 改用 API Key 方式,检查字段名 |
| ConcurrentModificationException | 遍历中外部修改容器 | 改用iterator.remove() |
| NoSuchElementException | next 越界 | 检查 hasNext 判断 |
| IllegalStateException | remove 时机错误 | 确保 next 后只调一次 remove |
关于 OAuth,有些工具默认走 OAuth 流程,但 TaoToken 的接入方式是 API Key。如果你在配置里看到 OAuth 相关字段,把它关掉或忽略,改用api_key字段。Claude Code 这类工具可能需要设置ANTHROPIC_API_KEY和ANTHROPIC_BASE_URL,Base URL 同样指向 TaoToken 的 API 地址。
还有一个容易忽略的点:迭代器的remove()和容器的remove()不是一回事。SimpleList如果对外暴露了remove(int index),调用方在遍历时调它,就会触发ConcurrentModificationException。正确做法是遍历时只用iterator.remove()。这个区别在 AI 生成的代码里有时会被混淆,建议生成后人工检查一遍。
如果你在排查过程中需要查文档,接入文档入口在 TaoToken 的 doc 页面。排障和接入相关的问题,优先看 API Keys 和接入文档;验证模型是否通,用模型对话;长期编码或 Agent 场景,看 Coding Plan。
6. 把迭代器模式用起来:从自定义容器到统一遍历接口
迭代器模式的价值,在你自己写容器的时候体现得最明显。JDK 的ArrayList、LinkedList、HashMap已经把迭代器实现得很完善,你直接用就行。但当你需要写一个环形缓冲区、一个分页数据源、一个树形结构,或者一个需要按业务规则过滤的集合时,就得自己实现Iterable和Iterator。
我在项目里用迭代器模式解决过一个分页遍历的问题。数据源每次只能拉一页,但调用方希望像遍历普通集合一样处理。做法是写一个PagedIterable,内部维护当前页码和页内游标,hasNext()判断当前页是否还有元素或是否还有下一页,next()返回元素并在页末自动拉取下一页。调用方只需要for (Item item : pagedIterable),完全不用关心分页细节。这就是迭代器模式「封装遍历算法」的直接收益。
另一个场景是多容器统一遍历。假设你有多个数据源,有的来自数据库,有的来自缓存,有的来自文件。你可以为每个数据源实现一个Iterator,然后用一个CompositeIterator把它们串起来,对外只暴露一个迭代器。调用方拿到这个迭代器后,遍历逻辑完全一致,不需要为每个数据源写不同的循环。
如果你想让 AI 帮你生成这类迭代器,建议把接口契约先写清楚,再让模型补实现。比如告诉它「hasNext()在游标越界时返回 false,next()在越界时抛NoSuchElementException,remove()在next()前调用抛IllegalStateException」。契约越明确,生成的代码越可靠。TaoToken 的统一 Key 让你在模型对话、Coding Plan、API 调用之间共享同一套配置,Base URL 固定为https://taotoken.net/api,不用反复切换。
最后给一个实用建议:写自定义迭代器时,优先用内部类实现。内部类可以直接访问容器的私有字段,不需要暴露get(int index)这类方法,封装性更好。JDK 的ArrayList.Itr就是这么做的。如果你的迭代器需要独立于容器存在,再考虑抽成单独的类。
迭代器模式本身不复杂,难的是把hasNext、next、remove三个方法的边界行为处理干净。把这篇里的SimpleList实现和测试用例跑一遍,再对照ArrayList的源码看一遍,基本就能掌握。遇到报错时,先分清是代码逻辑问题还是接入配置问题,再对症排查。