问题

什么是类型擦除?

答案

核心概念

类型擦除(Type Erasure) 是Java泛型的实现机制:编译器在编译期检查泛型类型,编译后擦除所有泛型信息,运行时泛型类型不存在。

类型擦除原理

1. 编译前的源代码

public class Box<T> {
    private T data;

    public void set(T data) {
        this.data = data;
    }

    public T get() {
        return data;
    }
}

// 使用
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get();

2. 编译后的字节码

// 类型参数T被擦除为Object
public class Box {
    private Object data; // T → Object

    public void set(Object data) { // T → Object
        this.data = data;
    }

    public Object get() { // T → Object
        return data;
    }
}

// 使用处插入强制类型转换
Box stringBox = new Box();
stringBox.set("Hello");
String value = (String) stringBox.get(); // 编译器自动插入强转

3. 字节码验证

使用 javap -c 查看字节码:

// 源码
public T get() {
    return data;
}

// 字节码
public java.lang.Object get();
  Code:
     0: aload_0
     1: getfield      #2  // Field data:Ljava/lang/Object;
     4: areturn

结论:泛型类型T在字节码中变成了Object。

擦除规则

规则1:无限定类型参数擦除为Object

// 源码
public class Box<T> {
    private T data;
}

// 擦除后
public class Box {
    private Object data; // T → Object
}

规则2:有限定类型参数擦除为限定类型

// 源码:上界限定
public class NumberBox<T extends Number> {
    private T data;

    public void process() {
        double value = data.doubleValue(); // 可以调用Number的方法
    }
}

// 擦除后
public class NumberBox {
    private Number data; // T extends Number → Number

    public void process() {
        double value = data.doubleValue();
    }
}
// 源码:多个上界
public class MyClass<T extends Comparable<T> & Serializable> {
    private T data;
}

// 擦除后(擦除为第一个边界)
public class MyClass {
    private Comparable data; // 擦除为Comparable
}

规则3:方法的类型参数也会被擦除

// 源码
public <T> T getFirst(List<T> list) {
    return list.get(0);
}

// 擦除后
public Object getFirst(List list) { // T → Object, List<T> → List
    return list.get(0);
}

类型擦除的影响

1. 运行时无法获取泛型类型

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

// 运行时类型相同
System.out.println(stringList.getClass() == intList.getClass()); // true

// 无法判断泛型类型
if (stringList instanceof List<String>) { // ❌ 编译错误
    // 非法的泛型类型
}

// 只能判断原始类型
if (stringList instanceof List) { // ✅ 正确
    // 合法
}

2. 不能创建泛型数组

// ❌ 错误:不能创建泛型数组
List<String>[] arrayOfLists = new List<String>[10]; // 编译错误

// ✅ 正确:可以创建原始类型数组
List[] arrayOfLists = new List[10];

// ✅ 或使用通配符
List<?>[] arrayOfLists = new List<?>[10];

原因

// 如果允许创建泛型数组,会导致类型不安全
Object[] arr = new List<String>[10]; // 假设允许
arr[0] = new ArrayList<Integer>(); // 数组协变,运行时类型擦除,无法检测
List<String> list = (List<String>) arr[0];
String s = list.get(0); // ClassCastException

3. 重载冲突

public class OverloadTest {
    // ❌ 错误:方法签名冲突
    public void process(List<String> list) { }
    public void process(List<Integer> list) { } // 编译错误

    // 原因:擦除后都变成 process(List list)
}
// ✅ 正确:通过不同参数个数或类型区分
public void process(List<String> list) { }
public void process(List<String> list, int flag) { }
public void process(Set<Integer> set) { }

4. 异常捕获限制

// ❌ 错误:不能捕获泛型异常
try {
    // ...
} catch (T e) { // 编译错误
    // ...
}

// ❌ 错误:泛型类不能继承Throwable
public class MyException<T> extends Exception { } // 编译错误

5. 静态上下文限制

public class Box<T> {
    // ❌ 错误:静态方法不能使用类的类型参数
    public static T staticMethod() { // 编译错误
        return null;
    }

    // ✅ 正确:静态方法可以有自己的类型参数
    public static <E> E genericStaticMethod() {
        return null;
    }
}

桥接方法(Bridge Method)

类型擦除会导致方法覆盖问题,编译器通过桥接方法解决。

// 父类
public class Node<T> {
    private T data;

    public void setData(T data) {
        this.data = data;
    }
}

// 子类
public class IntegerNode extends Node<Integer> {
    @Override
    public void setData(Integer data) {
        System.out.println("Setting integer: " + data);
        super.setData(data);
    }
}

擦除后

// 父类擦除后
public class Node {
    public void setData(Object data) { // T → Object
        this.data = data;
    }
}

// 子类擦除后
public class IntegerNode extends Node {
    // 程序员编写的方法
    public void setData(Integer data) {
        System.out.println("Setting integer: " + data);
        super.setData(data);
    }

    // 编译器自动生成的桥接方法
    public void setData(Object data) { // 保持多态
        setData((Integer) data); // 调用Integer版本
    }
}

验证桥接方法

IntegerNode node = new IntegerNode();
for (Method method : node.getClass().getDeclaredMethods()) {
    System.out.println(method.getName() + " - isBridge: " + method.isBridge());
}

// 输出:
// setData - isBridge: false  (程序员编写的方法)
// setData - isBridge: true   (编译器生成的桥接方法)

类型擦除的绕过方式

1. 通过反射获取泛型类型(有限)

// 方式1:通过父类的泛型信息
public abstract class BaseDao<T> {
    private Class<T> entityClass;

    @SuppressWarnings("unchecked")
    public BaseDao() {
        // 获取父类的泛型类型
        Type superclass = getClass().getGenericSuperclass();
        if (superclass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) superclass;
            Type[] typeArguments = parameterizedType.getActualTypeArguments();
            this.entityClass = (Class<T>) typeArguments[0];
        }
    }

    public T newInstance() throws Exception {
        return entityClass.newInstance();
    }
}

// 子类
public class UserDao extends BaseDao<User> {
    // entityClass = User.class
}

// 使用
UserDao dao = new UserDao();
User user = dao.newInstance(); // 可以创建User实例

2. 通过构造参数传递Class对象

public class Repository<T> {
    private Class<T> entityClass;

    public Repository(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    public T newInstance() throws Exception {
        return entityClass.newInstance();
    }

    public List<T> query(String sql) {
        // 使用entityClass进行JDBC查询
        return Collections.emptyList();
    }
}

// 使用
Repository<User> userRepo = new Repository<>(User.class);
User user = userRepo.newInstance();

类型擦除 vs C++模板

维度 Java泛型(类型擦除) C++模板
实现机制 编译期检查 + 运行时擦除 代码生成
字节码 只生成一份字节码 为每种类型生成代码
运行时类型 不存在 存在
性能 有装箱拆箱开销 无开销
代码膨胀 可能膨胀
向后兼容 兼容Java 5之前 N/A
// Java:只生成一份Box类的字节码
Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();
// stringBox.getClass() == intBox.getClass() → true
// C++:为String和int分别生成代码
Box<std::string> stringBox;
Box<int> intBox;
// typeid(stringBox) != typeid(intBox)

类型擦除的历史原因

Java在设计泛型时面临的约束:

  1. 向后兼容:必须兼容Java 5之前的代码
  2. JVM改动最小:不改变JVM字节码格式
  3. 库迁移成本:已有的集合类库需要平滑升级
// Java 5之前
List list = new ArrayList();
list.add("字符串");

// Java 5之后(类型擦除保证兼容)
List<String> list = new ArrayList<>();
list.add("字符串");
// 两者的字节码基本一致

实战陷阱

陷阱1:无法使用instanceof判断泛型

public <T> void process(Object obj) {
    // ❌ 错误
    if (obj instanceof T) { // 编译错误
        // ...
    }

    // ✅ 正确:传递Class对象
    public <T> void process(Object obj, Class<T> clazz) {
        if (clazz.isInstance(obj)) {
            T t = clazz.cast(obj);
        }
    }
}

陷阱2:泛型单例模式

// ❌ 错误:所有类型共享同一实例
public class Singleton<T> {
    private static Singleton<?> instance; // 静态字段

    public static <T> Singleton<T> getInstance() {
        if (instance == null) {
            instance = new Singleton<>();
        }
        return (Singleton<T>) instance;
    }
}

Singleton<String> s1 = Singleton.getInstance();
Singleton<Integer> s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true ← 同一个实例!

陷阱3:数组协变与泛型

// 数组是协变的
Object[] objArray = new String[10];
objArray[0] = 100; // ArrayStoreException(运行时检查)

// 泛型不是协变的
List<Object> objList = new ArrayList<String>(); // ❌ 编译错误

答题总结

类型擦除是Java泛型的实现机制:编译器在编译期进行类型检查,编译后擦除所有泛型信息,运行时泛型类型不存在。

擦除规则

  • 无限定类型参数擦除为Object(如TObject
  • 有限定类型参数擦除为限定类型(如T extends NumberNumber
  • 编译器自动插入类型转换和桥接方法

主要影响

  1. 运行时无法获取泛型类型信息
  2. 不能创建泛型数组
  3. 不能捕获泛型异常
  4. 静态上下文无法使用类的类型参数

类型擦除是为了向后兼容减少JVM改动而采用的折中方案,虽有局限但保证了平滑升级。可通过反射或传递Class对象部分绕过限制。