泛型的类型擦除(Type Erasure) 是 Java 泛型八股里非常高频的知识点。
先记一句话:
Java 的泛型主要存在于编译阶段,编译成字节码后,大部分泛型类型信息会被擦除。
1. 举个最直观的例子
你写:
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();源码层面明显不同:
List<String>
List<Integer>但是运行时:
System.out.println(list1.getClass() == list2.getClass());结果:
true因为运行时它们本质上都是:
ArrayList可以粗略理解成编译过程中:
List<String> ─┐
├── 类型擦除 ──→ List
List<Integer> ─┘所以 JVM 并不会因为一个是 String,一个是 Integer,就生成两个不同的 ArrayList 类。
2. 那泛型还有什么意义?
你可能会问:
既然最后都擦除了,那 List<String> 有什么用?
因为泛型最主要的作用发生在编译阶段。
例如:
List<String> list = new ArrayList<>();
list.add("hello"); // OK
list.add(123); // 编译报错编译器知道:
这是 List<String>
↓
只能放 String
↓
Integer 不允许因此:
泛型负责在编译阶段提供类型安全,类型检查完成后,再进行类型擦除。
**3. T
被擦除之后变成什么?
例如:
class Box<T> {
private T data;
public T getData() {
return data;
}
}类型擦除后,可以粗略理解成:
class Box {
private Object data;
public Object getData() {
return data;
}
}因为 T 没有限定,所以通常擦除成 Object。
但我们使用的时候:
Box<String> box = new Box<>();
String s = box.getData();为什么不用自己写:
String s = (String) box.getData();因为编译器会帮你处理必要的类型转换。
所以可以理解成:
程序员写:
String s = box.getData();
↓ 编译器处理
类似:
String s = (String) box.getData();4. 如果泛型有限制呢?
例如:
class Box<T extends Number> {
private T data;
}这里:
T extends Number那么擦除后,T 不再简单地变成 Object,而是以它的上界为擦除结果,可以粗略理解:
class Box {
private Number data;
}所以记:
<T> → Object
<T extends Number> → Number更准确地说,存在多个上界时,擦除使用最左边的上界。
5. 类型擦除带来的经典限制
因为:
List<String>
List<Integer>运行时泛型参数大部分已经被擦除了,所以不能这样判断:
if (obj instanceof List<String>) { // ❌
}通常只能:
if (obj instanceof List<?>) { // ✅
}同样,你也不能直接:
new T(); // ❌因为运行时并不知道这里的 T 具体应该创建什么类型。
面试怎么回答?
面试官问:
什么是 Java 泛型的类型擦除?
你可以回答:
Java 的泛型主要是在编译阶段实现的。编译器会利用泛型信息进行类型检查,保证类型安全;编译之后,大部分泛型类型信息会被擦除。
比如 List<String> 和 List<Integer> 在运行时实际上都是同一个 List 类型。如果泛型参数没有上界,通常会擦除为 Object;如果存在上界,比如 <T extends Number>,则会按照它的上界进行擦除。
编译器还会在需要的位置自动插入类型转换。因此 Java 泛型可以保证编译期类型安全,同时又能兼容泛型出现之前的代码。
最后压缩成一句口诀:
泛型:编译时检查,运行时擦除;无上界擦成 Object,有上界擦成对应上界。
不过要注意,“运行时完全没有任何泛型信息”这种说法也不严谨——类、字段、方法签名中有些泛型元数据仍可以通过反射获取。八股里说大部分泛型类型信息被擦除更准确。