在写 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 之后)。

遍历顺序:

  1. 先遍历数组 table[]

  2. 遇到桶里有链表 → 遍历链表

  3. 如果链表被树化 → 遍历红黑树节点

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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐