Java 集合遍历大扫盲:Iterator 的底层原理与 Iterable 的设计美学

📅 2026/7/31 9:02:18 👁️ 阅读次数 📝 编程学习
Java 集合遍历大扫盲:Iterator 的底层原理与 Iterable 的设计美学

目录

  • 1. 引言:从 for 循环到 foreach 的疑惑
  • 2. 回顾:Java 集合遍历的常用方式
    • 2.1 普通 for 循环(基于索引)
    • 2.2 增强 for 循环(foreach)
    • 2.3 Iterator 显式遍历
    • 2.4 Java 8 的 forEach + Lambda
  • 3. Iterator 接口的底层原理
    • 3.1 Iterator 定义
    • 3.2 迭代器模式
    • 3.3 ArrayList 中 Iterator 的实现(源码剖析)
    • 3.4 fail‑fast 与 fail‑safe
  • 4. Iterable 接口的设计美学
    • 4.1 为什么需要 Iterable?
    • 4.2 设计美感:接口分离
    • 4.3 Java 8 对 Iterable 的增强
  • 5. 手写一个可被 foreach 遍历的自定义集合
  • 6. 常见面试题与避坑指南
    • 6.1 为什么 foreach 遍历集合时不能直接使用集合的 remove 方法?
    • 6.2 Map 如何用 foreach 遍历?
    • 6.3 迭代器遍历时修改集合的安全方法有哪些?
  • 7. 总结

1. 引言:从 for 循环到 foreach 的疑惑

在日常 Java 开发中,我们经常使用for (Object obj : list)这样的语法遍历集合。这种写法简洁优雅,但很多人并不清楚它的背后究竟发生了什么。为什么List可以这样遍历?为什么自定义的类不能直接用foreach?这一切的秘密都藏在两个核心接口里:IteratorIterable

本文将带你从最基础的集合遍历方式出发,逐步深入Iterator的底层原理,剖析Iterable的设计美学,并手写一个可被foreach遍历的自定义集合,真正做到“知其然,更知其所以然”。

2. 回顾:Java 集合遍历的常用方式

在理解原理之前,我们先快速回顾 Java 中常见的几种集合遍历方法:

2.1 普通 for 循环(基于索引)

List<String>list=newArrayList<>();list.add("a");list.add("b");list.add("c");for(inti=0;i<list.size();i++){System.out.println(list.get(i));}

这种方式只对实现了RandomAccess接口的List友好。LinkedList使用get(i)会非常低效。

2.2 增强 for 循环(foreach)

for(Strings:list){System.out.println(s);}

这是目前最简洁的写法,但其实它是Iterator的语法糖。

2.3 Iterator 显式遍历

Iterator<String>it=list.iterator();while(it.hasNext()){Strings=it.next();System.out.println(s);}

2.4 Java 8 的 forEach + Lambda

list.forEach(s->System.out.println(s));

这种方式直接传入消费者函数,底层仍然依赖于IteratorSpliterator

3. Iterator 接口的底层原理

3.1 Iterator 定义

java.util.Iterator接口非常简单,核心方法只有三个:

publicinterfaceIterator<E>{booleanhasNext();Enext();defaultvoidremove(){thrownewUnsupportedOperationException("remove");}}
  • hasNext():判断是否还有下一个元素。
  • next():返回当前元素并将游标后移。
  • remove():移除上一次next()返回的元素(默认不支持)。

3.2 迭代器模式

Iterator的设计正是经典的迭代器模式:将集合的遍历行为从集合本身分离出来,封装在一个独立的迭代器对象中。这样做的好处是:

  • 职责单一:集合只负责存储数据,迭代器只负责遍历数据。
  • 封装变化:集合的内部结构(数组、链表、树、哈希表)可以千变万化,但对调用方来说,只需要拿到一个Iterator,用相同的hasNext()/next()接口即可遍历。
  • 支持并发迭代:可以同时有多个迭代器指向同一个集合的不同位置且互不干扰。

3.3 ArrayList 中 Iterator 的实现(源码剖析)

ArrayListiterator()方法会返回一个内部类Itr,它实现了Iterator接口。我们来看看关键代码(基于 JDK 8,精简处理):

privateclassItrimplementsIterator<E>{intcursor;// 下一个要返回元素的索引intlastRet=-1;// 上一个返回元素的索引,-1 表示没有intexpectedModCount=modCount;publicbooleanhasNext(){returncursor!=size;}publicEnext(){checkForComodification();inti=cursor;if(i>=size)thrownewNoSuchElementException();Object[]elementData=ArrayList.this.elementData;if(i>=elementData.length)thrownewConcurrentModificationException();cursor=i+1;return(E)elementData[lastRet=i];}finalvoidcheckForComodification(){if(modCount!=expectedModCount)thrownewConcurrentModificationException();}}
  • cursor像一个游标,始终指向下一次next()应该返回的元素索引。
  • lastRet记录上一次next()返回的索引,方便remove()时删除。
  • expectedModCount是经典的**快速失败(fail‑fast)**机制。如果在迭代过程中,有其他线程(或同一线程通过list.remove()等方法)修改了集合的结构,modCount就会变化,导致下一次next()remove()时抛出ConcurrentModificationException。这个机制可以尽早暴露出多线程下的不安全修改。

3.4 fail‑fast 与 fail‑safe

  • fail‑fast:迭代过程中检测到集合结构被修改,立即抛出异常。ArrayListHashMap等大部分集合都采用这种策略。
  • fail‑safe:迭代时操作的是集合的一个快照,允许并发修改。例如CopyOnWriteArrayListConcurrentHashMap的迭代器就是这种类型。

4. Iterable 接口的设计美学

4.1 为什么需要 Iterable?

有了Iterator是不是就够了?为什么还要一个Iterable?关键就在于foreach语法的要求。

publicinterfaceIterable<T>{Iterator<T>iterator();}

Iterable接口只有一个抽象方法iterator(),它返回一个Iterator对象。这就是工厂方法模式的应用:Iterable是迭代器工厂(生产Iterator),而Iterator是具体的迭代工具。

JVM 在编译foreach循环时,会检查被遍历的对象是否实现了Iterable接口。只有实现了Iterable,编译器才会将其“语法糖”转换为对iterator()hasNext()next()的调用。

4.2 设计美感:接口分离

IteratorIterable的分离堪称设计美学的典范:

  • 可遍历性(Iterable)遍历状态(Iterator)解耦。一个Iterable对象可以多次调用iterator()生成全新的Iterator,每个Iterator都拥有自己的遍历状态(游标),互不影响。这样便实现了可重复、可并行的遍历。
  • 对于调用方而言,我只需要知道对象是Iterable,就能使用foreach,完全不用关心底层实现是数组还是链表。
  • 这种设计完美诠释了“面向接口编程”的理念,大大提升了代码的复用性和扩展性。

4.3 Java 8 对 Iterable 的增强

在 Java 8 中,Iterable新增了两个默认方法:

defaultvoidforEach(Consumer<?superT>action){Objects.requireNonNull(action);for(Tt:this){action.accept(t);}}defaultSpliterator<T>spliterator(){returnSpliterators.spliteratorUnknownSize(iterator(),0);}
  • forEach()让集合可以直接使用 Lambda 表达式进行内部迭代,进一步简化代码。
  • spliterator()提供了可分割迭代器,为并行流处理打下基础。

5. 手写一个可被 foreach 遍历的自定义集合

理论说再多都不如亲手实践。下面我们来编写一个简单的单向链表MyLinkedList,并让它支持foreach遍历。

importjava.util.Iterator;importjava.util.NoSuchElementException;// 实现 Iterable 接口publicclassMyLinkedList<T>implementsIterable<T>{privateNode<T>head;privateintsize;privatestaticclassNode<T>{Tdata;Node<T>next;Node(Tdata){this.data=data;}}publicvoidadd(Tdata){Node<T>newNode=newNode<>(data);if(head==null){head=newNode;}else{Node<T>current=head;while(current.next!=null){current=current.next;}current.next=newNode;}size++;}publicintsize(){returnsize;}// 核心方法:返回迭代器@OverridepublicIterator<T>iterator(){returnnewLinkedListIterator();}// 内部类实现 Iterator 接口privateclassLinkedListIteratorimplementsIterator<T>{privateNode<T>current=head;// 游标@OverridepublicbooleanhasNext(){returncurrent!=null;}@OverridepublicTnext(){if(!hasNext()){thrownewNoSuchElementException();}Tdata=current.data;current=current.next;returndata;}}// 测试publicstaticvoidmain(String[]args){MyLinkedList<String>list=newMyLinkedList<>();list.add("Java");list.add("Python");list.add("Go");// 因为实现了 Iterable,可以直接用 foreachfor(Stringlang:list){System.out.println(lang);}}}

这段代码完美呈现了IterableIterator的协作模式:MyLinkedList通过实现Iterable获得了“可遍历”的身份,而遍历的具体逻辑被封装在内部类LinkedListIterator中。哪怕链表的结构再复杂,外部只需调用iterator()即可得到一个“标准化”的遍历器。

6. 常见面试题与避坑指南

6.1 为什么 foreach 遍历集合时不能直接使用集合的 remove 方法?

这会导致modCount改变而expectedModCount未变,从而触发ConcurrentModificationException。正确的做法是使用Iterator自己的remove()方法。

Iterator<String>it=list.iterator();while(it.hasNext()){Strings=it.next();if("remove_me".equals(s)){it.remove();// 正确用法}}

6.2 Map 如何用 foreach 遍历?

Map本身没有实现Iterable,但它的keySet()values()entrySet()返回的Set都实现了Iterable,所以你可以:

for(Map.Entry<K,V>entry:map.entrySet()){System.out.println(entry.getKey()+" = "+entry.getValue());}

6.3 迭代器遍历时修改集合的安全方法有哪些?

  • 使用Iterator.remove()
  • 使用线程安全的并发集合,如CopyOnWriteArrayList
  • 使用 Java 8 的Collection.removeIf(),它内部使用了Iterator

7. 总结

本文从最简单的foreach语法出发,一步步挖掘出背后IteratorIterable的协作关系。Iterator用一种优雅的状态机模式封装了遍历行为,而Iterable作为其工厂接口,让每种集合都拥有了统一的可遍历契约。这种设计不仅降低了代码耦合度,也为现代 Java 集合框架的灵活扩展奠定了基础。

理解了这些底层原理后,无论是阅读源码还是应对面试,你都能更加胸有成竹。如果你觉得有帮助,欢迎点赞、收藏,也欢迎在评论区留下你的疑问或见解。