上一篇我们聊了泛型的基础——怎么用。
今天,我们来揭开泛型的底层原理。
一、泛型的“假象”——类型擦除(Type Erasure)
你有没有想过:为什么 Java 的泛型不能用于 new T()
或判断 if (obj instanceof T)
?
答案就藏在 Java 泛型的核心机制中:类型擦除(Type Erasure)。
1. 什么是类型擦除?
简单说:泛型只存在于编译期,运行时会被“擦掉”。
Java 的泛型是通过“类型擦除”实现的,也就是说:
- ✅ 编译时:编译器用泛型做类型检查,确保类型安全。
- ❌ 运行时:JVM 根本不知道泛型的存在,所有的泛型信息都被“擦除”了。
举个例子:
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 问:这两个 List 的运行时类型一样吗?
System.out.println(strList.getClass() == intList.getClass()); // 输出:true
你没看错!输出是 true
!
因为在运行时,List<String>
和 List<Integer>
都变成了 List
,泛型信息被擦除了。
2. 擦除后变成什么?
- 如果没有限定类型(比如
<T>
),擦除后变成Object
。 - 如果有限定类型(比如
<T extends Number>
),擦除后变成上限类型(这里是Number
)。
public class Box<T extends Number> {
private T value;
public T getValue() { return value; }
}
编译后相当于:
public class Box {
private Number value; // T 被擦除为 Number
public Number getValue() { return value; }
}
3. 为什么要类型擦除?
这是 Java 为了兼容老版本(Java 5 之前没有泛型)而做的设计。它让泛型代码可以和非泛型代码共存,但代价是:
- 不能在运行时获取泛型类型
- 不能
new T()
- 不能
instanceof T
二、通配符的上下限:? extends T
和 ? super T
泛型中有一个非常重要的概念:通配符(Wildcard),尤其是它的上下限用法。
1. ? extends T
—— 上限通配符(Upper Bounded Wildcard)
表示“某种类型,它是 T 或 T 的子类”。
List<? extends Number> list;
这个 list
可以是:
List<Number>
List<Integer>
List<Double>
- ……
但你不能往里面添加元素(除了 null
),因为编译器不知道具体是哪种子类型。
list.add(new Integer(1)); // ❌ 编译错误!
Number n = list.get(0); // ✅ 可以读取,因为一定是 Number 或其子类
👉 适用场景:只读操作(Producer)
2. ? super T
—— 下限通配符(Lower Bounded Wildcard)
表示“某种类型,它是 T 或 T 的父类”。
List<? super Integer> list;
这个 list
可以是:
List<Integer>
List<Number>
List<Object>
你可以往里面添加 Integer
或其子类:
list.add(new Integer(1)); // ✅ 可以添加
Object obj = list.get(0); // ❌ 只能拿到 Object,类型信息丢失
👉 适用场景:只写操作(Consumer)
三、PECS 原则:搞定上下限的“黄金口诀”
记不住什么时候用 extends
,什么时候用 super
?记住这个口诀:
PECS = Producer-Extends, Consumer-Super
- 如果你从集合中读取数据(它是“生产者”),用
? extends T
- 如果你往集合中写入数据(它是“消费者”),用
? super T
举个 JDK 中的经典例子:Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src)
src
是“生产者” → 用? extends T
(从里面读 T 类型的数据)dest
是“消费者” → 用? super T
(往里面写 T 类型的数据)
完美符合 PECS!
四、泛型的局限性(你知道吗?)
由于类型擦除,Java 泛型有一些“坑”:
限制 | 说明 |
---|---|
❌ 不能创建泛型数组 | new T[] 不合法,因为运行时不知道 T 是什么 |
❌ 不能用于基本类型 | List<int> 不行,要用 List<Integer> |
❌ 不能用于 instanceof |
obj instanceof List<String> 不合法 |
❌ 静态变量不能使用泛型类型 | private static T t; 不合法,因为静态变量属于类,而泛型是实例相关的 |
五、面试高频问题(附参考回答)
Q1:Java 泛型是怎么实现的?为什么叫“伪泛型”?
A:Java 泛型是通过“类型擦除”实现的,泛型信息只在编译期存在,运行时会被擦除。因此它被称为“伪泛型”,与 C# 的“真泛型”不同。
Q2:List<Object>
和 List<String>
有继承关系吗?
A:没有!
List<String>
不是List<Object>
的子类。这是泛型的“不可变性”。如果需要兼容,要用通配符,比如List<? extends Object>
。
Q3:? extends T
和 ? super T
有什么区别?什么时候用?
A:
? extends T
表示 T 及其子类,适合“读取”;? super T
表示 T 及其父类,适合“写入”。遵循 PECS 原则:生产者用extends
,消费者用super
。
六、总结:泛型的本质
概念 | 关键点 |
---|---|
类型擦除 | 泛型只在编译期存在,运行时被擦除为 Object 或上限类型 |
通配符 | ? 表示未知类型,extends 是上限,super 是下限 |
PECS 原则 | 生产者用 extends ,消费者用 super |
局限性 | 不能 new T、不能用基本类型、不能 instanceof 等 |
写在最后
Java 泛型看似简单,实则暗藏玄机。理解了类型擦除和PECS 原则,你就掌握了泛型的“内功心法”。
下次面试官再问:“说说泛型的原理”,你就可以从容不迫地说:
“Java 泛型基于类型擦除实现,编译期检查类型安全,运行时擦除泛型信息。为了灵活处理类型兼容,我们使用
? extends T
和? super T
,并遵循 PECS 原则……”