泛型与类型擦除

面试官问:"Java 泛型的类型擦除是什么?"

候选人小何答:"泛型信息在编译后会被擦除,运行时不保留。"

面试官追问:"那 List<String>List<Integer> 在运行时的 Class 对象是什么?"

小何说:"都是 List,不带泛型参数。"

面试官又问:"那为什么 new ArrayList<String>() 不能 add(Integer)?"

小何卡住了。

【面试官心理】 类型擦除是 Java 泛型的核心机制,但大多数人只知道"编译后擦除"这个结论,不知道具体是怎么擦除的、擦除后发生了什么。能说出"桥接方法"和"类型检查点"的候选人,才是真正理解过泛型实现的人。

一、类型擦除的本质 🔴

1.1 擦除前 vs 擦除后

// 编译前
public class Container<T> {
    private T value;

    public T getValue() { return value; }

    public void setValue(T value) { this.value = value; }
}

编译后(类型擦除):

// 编译后:T 被替换为 Object
public class Container {
    private Object value; // T → Object

    public Object getValue() { return value; }

    // 桥接方法:为了保持多态
    @Override
    public String getValue() { return (String) this.value; } // ← 桥接方法

    public void setValue(Object value) { this.value = value; }
}

1.2 类型擦除的两种情况

泛型上限擦除为例子
无限制(<T>ObjectTObject
有上限(<T extends Number>上限类型T extends NumberNumber
class NumericBox<T extends Number> { // 有上限
    private T value;
}

// 擦除后:
class NumericBox {
    private Number value; // T → Number(不是 Object)
}

1.3 擦除后的类型检查

List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // ❌ 编译错误

// 编译器在编译时插入了类型检查:
public boolean add(E e) {
    if (e instanceof String) { // ← 编译器插入的检查
        // ...
    }
    throw new ArrayStoreException();
}

类型检查发生在编译期,而不是运行期。

二、泛型与多态的矛盾 🔴

2.1 子类泛型的类型不一致

class Parent<T> {
    T value;
    void set(T v) { this.value = v; }
}

class Child extends Parent<String> {
    // value 的类型在子类中已经是 String 了
    // 但父类的 set 方法签名是 set(T) → set(Object)

    @Override
    void set(String v) { this.value = v; } // 合法的重写
}

2.2 桥接方法的必要性

// 父类(擦除后)
class Parent {
    Object value;
    void set(Object v) { this.value = v; }
}

// 子类
class Child extends Parent<String> {
    String value; // 子类中 value 是 String,不是 Object

    // 编译器自动生成桥接方法:
    @Override
    void set(Object v) {
        this.set((String) v); // 委托给 set(String)
    }

    void set(String v) { this.value = v; }
}

// 多态调用:
Parent p = new Child();
p.set("hello"); // 调用桥接方法 set(Object) → set(String)
p.set(123);     // 编译错误(类型检查)
💡

能用字节码工具看到"桥接方法"的候选人,面试官会认为有深入研究。这是 P6+ 的加分点。

三、泛型的局限 🔴

3.1 不能用基本类型

// ❌ 编译错误
List<int> list = new ArrayList<int>();

// ✅ 只能用包装类型
List<Integer> list = new ArrayList<>();

原因:类型擦除后变成 Object,基本类型不是对象,无法对应。

3.2 不能 new 泛型数组

// ❌ 编译错误
T[] array = new T[10];

// ✅ 可以用反射创建
T[] array = (T[]) Array.newInstance(cls, 10);

// ✅ 可以声明,但初始化需要反射
class Container<T> {
    T[] array;
    Container(Class<T> cls, int size) {
        this.array = (T[]) Array.newInstance(cls, size);
    }
}

3.3 不能实例化泛型类型

// ❌ 编译错误
T obj = new T();

// ✅ 可以用反射
T obj = cls.getDeclaredConstructor().newInstance();

3.4 不能用 instanceof

// ❌ 编译错误
if (obj instanceof List<String>) { } // 编译失败

// ✅ 可以使用(不带泛型)
if (obj instanceof List) { } // 运行时会检查这个

3.5 不能重载方法签名相同但泛型参数不同的方法

// ❌ 编译错误:擦除后方法签名相同
class Container {
    void process(List<String> list) { }
    void process(List<Integer> list) { } // 擦除后都是 process(List)
}

四、泛型与数组 🔴

4.1 不能创建泛型数组

// ❌ 编译错误
List<String>[] array = new ArrayList<String>[10];

// 为什么不行?
// List<String>[] 看起来可以放 List<String>
// 但擦除后变成 List[],运行时可能放 List<Integer>
// 导致 ClassCastException

// ✅ 可以创建泛型数组的原始类型
List<String>[] array = new ArrayList[10]; // 警告但不报错
array[0] = new ArrayList<String>();
array[1] = new ArrayList<Integer>(); // 编译通过,但运行时不安全!
⚠️

泛型数组的原始类型是类型安全隐患。生产中如果看到这种代码,需要 review 确保不会有类型安全问题。

五、生产避坑

5.1 泛型擦除导致的 ClassCastException

// 危险代码:
public class GenericContainer {
    private List<String> list = new ArrayList<>();

    public void add(Object item) {
        // 编译警告,但能通过
        list.add((String) item);
    }
}

// 触发:
GenericContainer container = new GenericContainer();
container.add(123); // 编译时只警告,运行时抛出 ClassCastException

5.2 Jackson 序列化泛型问题

// Jackson 的 TypeReference 用于保留泛型类型
// ❌ 错误:类型擦除后丢失泛型信息
User user = objectMapper.readValue(json, User.class); // 泛型信息丢失

// ✅ 正确:使用 TypeReference
User user = objectMapper.readValue(json, new TypeReference<User>() {});
// TypeReference 内部类保留了泛型类型

六、追问升级

面试官:"List<Object>List<String> 是什么关系?"

// ❌ 编译错误
List<Object> objects = new ArrayList<String>();

// List<String> 不是 List<Object> 的子类!
// 原因:如果是的话:
objects.add(123); // 合法(Object 可以是 Integer)
String s = objects.get(0); // ClassCastException!

// 这叫做"协变性问题",Java 选择了类型安全

面试官:"泛型的应用场景有哪些?"

// 1. 集合类:List<T>, Map<K, V>, Set<T>
List<String> list = new ArrayList<>();

// 2. 通用工具类:Class<T>
public <T> T parse(String json, Class<T> clazz) {
    return objectMapper.readValue(json, clazz);
}

// 3. 通用算法:Comparable<T>
public class Sorter<T extends Comparable<T>> {
    public void sort(T[] array) {
        // T 实现了 Comparable 接口,保证有 compareTo 方法
    }
}

// 4. Lambda/Stream 的函数式接口
Function<String, Integer> parser = Integer::parseInt;

【面试官心理】 能说出"协变性"和"类型安全"作为 List<Object>List<String> 不能赋值的原因,说明候选人对泛型的设计哲学有深入理解。这是 P6 的进阶点。