Java基础篇——Java 迭代器 + 泛型,一次性彻底讲清楚(终于不再懵了)
在写 Java 集合的时候,我总感觉自己对“迭代器”、“泛型”、“类型擦除”这些概念懂了个大概,但一用就懵逼。
直到我把源码翻了个底朝天,这些东西才真正串起来。
今天就结合我最近的学习,把迭代器和泛型用最通俗、最接地气的方式讲一下,保证你看完之后就彻底通了。
一、迭代器(Iterator)到底是干嘛用的?
说白了就是让我们对多种数据结构的遍历能用采用一种方式来实现。
你可能听过一句话:
Java 一共有很多种集合,但遍历它们只需要一种方式 —— 迭代器。
这就是迭代器存在的意义。
如果没有迭代器,那我们遍历 10 种集合就要学 10 套遍历方式:
-
ArrayList:用索引遍历
-
LinkedList:不能随便用索引,要链表走指针
-
HashSet:内部是 HashMap,还不能按顺序取
-
TreeSet:基于红黑树
……
学完估计脑壳都炸了。
于是 Java 设计了一个统一的遍历接口:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
其实就是通过三个步骤:判断是否有,拿到元素,考虑是否要删除。
只要集合实现 Iterator,你就能用一种统一方式遍历所有集合。
二、Java 集合迭代器的底层原理(重点!)
下面说人话讲清楚:不同集合类实现的是同一个 Iterator 接口,但内部遍历方式完全不同。
1. ArrayList 的迭代器:基于数组索引
ArrayList 底层是数组,所以它的迭代器原理非常简单:
-
维护一个光标(cursor)记录当前位置
-
每次 next() 就取
elementData[cursor] -
然后 cursor++
伪代码:
class Itr implements Iterator<E> {
int cursor = 0;
public boolean hasNext() {
return cursor != size;
}
public E next() {
return elementData[cursor++];
}
}
优点: 超快
缺点: 遍历期间结构修改可能抛 ConcurrentModificationException
2. LinkedList 的迭代器:基于链表节点指针
LinkedList 底层结构:
prev <-> Node <-> next
它的迭代器靠的是节点指针移动:
-
保存当前节点(first node)
-
next() 就访问当前 node.item 并移动到 node.next
-
hasNext() 判断 node != null
伪代码:
Node<E> lastReturned;
Node<E> next;
public boolean hasNext() {
return next != null;
}
public E next() {
lastReturned = next;
next = next.next;
return lastReturned.item;
}
特点:
-
不像数组能随机访问
-
指针一步步走
-
对链表数据结构友好
3. HashMap 的迭代器:先走数组,再走链表 / 红黑树
HashMap 更复杂,它底层是数组 + 链表 + 红黑树(JDK8 之后)。
遍历顺序:
-
先遍历数组 table[]
-
遇到桶里有链表 → 遍历链表
-
如果链表被树化 → 遍历红黑树节点
HashMap 的迭代器逻辑大概这样:
for (int i = 0; i < table.length; i++) {
Node node = table[i];
while (node != null) {
visit(node);
node = node.next; // 或者后继节点(红黑树)
}
}
特点:
-
遍历顺序和 key 的 hash 值有关 → 不保证顺序
-
可以遍历 key、value、entry 三种视图
小结一句话:
迭代器提供 统一接口,具体怎么遍历由集合内部“自己实现”。
ArrayList 用索引,LinkedList 用指针,HashMap 用桶 + 链表/树。
三、增强 for 的底层其实就是迭代器
本质上来说增强for是基于迭代器来实现的,所以 迭代器 ——> foreach , 例如:
for (String s : list) {
System.out.println(s);
}
其实会被编译器翻译成:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
所以:
增强 for = 语法糖,本质还是迭代器。
四、Lambda 遍历 vs forEach 遍历 vs 迭代器
你写的 Lambda:
list.forEach(item -> System.out.println(item));
这个item -> 中的item是传递的参数。可以理解为有一个函数(item)-> 传递了item参数执行了sout的代码,只不过这里()省略掉了。
本质是:
-
还是调用 iterator 遍历
-
Lambda 只是一个回调函数(Consumer)
等价于:
for (String item : list) { ... }
等价于:
Iterator<String> it = list.iterator();
while(it.hasNext()) ...
遍历永远是迭代器。Lambda 只是让你写回调更爽。
五、泛型到底是什么?T 到底是啥?
泛型最核心的两句话:
1. T 是类型的占位符(定义阶段)
你写:
class Box<T> {
T value;
}
T 只是告诉编译器:
这里会放一种类型,但我暂时不知道是什么类型。
此时 T 可以代表任何类型。
2. 使用泛型时必须告诉它具体类型(使用阶段)
Box<String> box = new Box<>();
此时所有的 T 替换成 String。
所以 T 是“模板”,不是类型本身。
而 String/Integer/User 才是具体类型。
六、泛型的两个好处
简单来说就是安全+简洁。
① 类型安全(编译阶段就检查)
没泛型时代:
List list = new ArrayList();
list.add("hello");
Integer x = (Integer) list.get(0); // 运行时崩了
有泛型之后:
List<String> list = new ArrayList<>();
list.add("hello");
编译器直接挡住类型错误:
Error: cannot add Integer to List<String>
你甚至还没运行代码,就避免了 ClassCastException。
② 代码复用(不用写一堆重复类)
原来如果想做:
-
StringBox
-
IntegerBox
-
UserBox
你要写 3 个类。
现在:
class Box<T> {}
一个类搞定所有类型。
七、类型擦除到底是什么?
简单来说,泛型只在编译器眼里存在,一旦生成字节码,泛型就被擦掉了,运行时 JVM 根本不知道泛型这回事。
什么意思?
你写:
List<String> list = new ArrayList<>();
编译之后,其实会变成:
List list = new ArrayList();
String 的类型信息没了。
那我们为什么还要擦除类型呢?
八、为什么 Java 要擦除泛型?看似矛盾其实很合理
简单来说就是考虑到了对旧版本的兼容性。
Java 5 才引入泛型,而 JVM 早就存在了。
为了保证:
-
旧版本 JVM 可以跑新泛型代码
-
旧代码(没有泛型)还能继续使用 ArrayList 等集合
Java 只能选择 在编译器做类型检查,运行时把泛型擦掉。
这就是类型擦除的根本目的:
兼容性。
如果不擦除泛型,整个 JVM 都得改,所有旧代码全部报废。
Java 绝对不敢这么干。
九、本文总结
-
Iterator 定义了统一遍历接口
-
不同集合按自己结构实现不同遍历逻辑
-
增强 for = 语法糖,底层是迭代器
-
forEach + Lambda = 还是基于 iterator 遍历
-
泛型分两个阶段:定义时占位,使用时必须指定类型
-
泛型的类型信息只在编译期存在,运行时会被擦除
-
类型擦除的本质原因:为了兼容旧 JVM
更多推荐


所有评论(0)