Skip to content

类型擦除

什么是类型擦除?

这是 Java 泛型最核心、也最容易让人困惑的概念。

类型擦除:泛型信息只存在于编译期,运行时 JVM 会把泛型擦除,用原始类型(第一个边界或 Object)替代。

换句话说:泛型是给编译器用的,不是给 JVM 用的。

看一个例子:

java
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // true!

stringListintList 在运行时是同一个类型——都是 ArrayList。泛型 <String><Integer> 在编译完成后就消失了。

擦除规则

类型擦除按以下规则进行:

泛型类型擦除后
TObject
T extends NumberNumber
?Object
? extends TT
? super TObject

无边界的 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
  • 编译器会生成桥接方法保持多态

理解擦除,才能理解泛型的能力和局限。

基于 VitePress 构建