类型擦除
什么是类型擦除?
这是 Java 泛型最核心、也最容易让人困惑的概念。
类型擦除:泛型信息只存在于编译期,运行时 JVM 会把泛型擦除,用原始类型(第一个边界或 Object)替代。
换句话说:泛型是给编译器用的,不是给 JVM 用的。
看一个例子:
java
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true!stringList 和 intList 在运行时是同一个类型——都是 ArrayList。泛型 <String> 和 <Integer> 在编译完成后就消失了。
擦除规则
类型擦除按以下规则进行:
| 泛型类型 | 擦除后 |
|---|---|
T | Object |
T extends Number | Number |
? | Object |
? extends T | T |
? super T | Object |
无边界的 T → Object
java
public class Box<T> {
private T content;
public T get() {
return content;
}
}编译后擦除为:
java
public class Box {
private Object content; // T 变成 Object
public Object get() {
return content;
}
}有边界的 T → 边界类型
java
public class NumberBox<T extends Number> {
private T value;
public T get() {
return value;
}
}编译后擦除为:
java
public class NumberBox {
private Number value; // T 变成 Number
public Number get() {
return value;
}
}擦除带来的问题
问题一:泛型不能直接用 instanceof
java
List<String> list = new ArrayList<>();
if (list instanceof List<String>) { // 编译错误!
// ...
}因为运行时没有 List<String>,只有 List。
JDK 14+ 有有限支持:
java
// JDK 16+ 支持
if (list instanceof List<String> s) {
// s 会被自动推断为 String
System.out.println(s.get(0).toLowerCase());
}问题二:不能创建泛型数组
java
public <T> T[] createArray() {
return new T[10]; // 编译错误!
}因为 new T[10] 擦除后变成 new Object[10],但返回类型是 T[],需要强制转换。
解决方案:
java
// 方案一:传入 Class 对象
public <T> T[] createArray(Class<T> clazz, int size) {
return (T[]) java.lang.reflect.Array.newInstance(clazz, size);
}
// 方案二:使用 ArrayList
public <T> List<T> createList() {
return new ArrayList<>();
}问题三:不能 new T()
java
public <T> T create() {
return new T(); // 编译错误!
}同上,擦除后变成 new Object(),类型不安全。
解决方案:使用反射传入 Class:
java
public <T> T create(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}问题四:泛型类的静态成员不能用 T
java
public class Container<T> {
private static T instance; // 编译错误!
public static T getInstance() { // 编译错误!
return instance;
}
}因为 T 属于实例,而静态成员属于类。不同实例可能有不同的 T,但静态成员只有一份。
桥接方法
编译器会在必要时生成桥接方法,保持多态一致性:
java
// 源码
public class Node<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// 子类
public class StringNode extends Node<String> {
@Override
public void set(String value) {
// ...
}
}编译后,编译器会为 Node 生成桥接方法:
java
public class Node {
private Object value;
public void set(Object value) { // 原始方法
this.value = value;
}
public Object get() {
return value;
}
// 桥接方法,保持多态
public void set(String value) {
this.set((Object) value); // 调用原始方法
}
public String get() {
return (String) this.get(); // 强制转换
}
}这保证了 StringNode.set("hello") 能正确调用到 Node.set(Object)。
擦除与数组
泛型和数组有微妙的互动:
java
// 运行时数组需要具体类型
String[] arr = new String[10];
arr[0] = "hello";
arr[1] = 123; // 编译错误
// 泛型数组?不行
List<String>[] lists = new ArrayList<String>[10]; // 编译错误
List<String>[] lists = new ArrayList[10]; // 警告但不报错,但类型不安全什么时候需要关心类型擦除?
需要泛型反射时
java
// 获取泛型父类的类型参数
public class Sub extends Parent<String, Integer> {}
Type type = Sub.class.getGenericSuperclass();
if (type instanceof ParameterizedType pt) {
Type[] args = pt.getActualTypeArguments();
System.out.println(args[0]); // class java.lang.String
System.out.println(args[1]); // class java.lang.Integer
}泛型和序列化时
如果用 Gson 等库序列化泛型对象,需要提供 TypeToken:
java
// TypeToken 让 Gson 知道具体类型
TypeToken<List<String>> listType = new TypeToken<List<String>>(){};
List<String> list = new Gson().fromJson(json, listType);总结
类型擦除是 Java 泛型的核心机制:
泛型只在编译期存在,运行时用原始类型替代。
这意味着:
- 泛型不能用于
instanceof(JDK 16+ 部分支持) - 不能
new T()或new T[] - 静态方法不能用类级泛型
T - 编译器会生成桥接方法保持多态
理解擦除,才能理解泛型的能力和局限。
