Java面试有一个经典陷阱题,我当年就栽过。

面试官问:"ArrayList<String>和ArrayList<Integer>,它们的Class对象一样吗?"

我脱口而出:"不一样吧,一个是String的列表,一个是Integer的列表。"

面试官笑了:"你回去查一下getClass()。"

后来我查了才知道——完全一样。不管是new ArrayList<String>()还是new ArrayList<Integer>(),跑list.getClass()得到的都是ArrayList.class,没有泛型参数信息。

从那以后我开始认真研究泛型擦除。结果发现搞懂擦除之后,不只这道题,一连串以前模模糊糊的概念全通了。

一、擦除到底是什么?

泛型是Java 5才引入的。为了兼容没有泛型的旧代码,Java设计了一个妥协方案:编译时检查类型安全,运行时删除所有泛型类型信息。这个"编译时有,运行时无"的机制,就是类型擦除。

看个最简单的例子:

List<String> list = new ArrayList<>();

编译后字节码里这行变成了:

List list = new ArrayList();

<String>完全消失了,所有泛型参数都被替换成它们的上界(默认是Object)。所以ArrayList<String>和ArrayList<Integer>在运行时长得一模一样——都是裸的ArrayList。

二、擦除带来的三个"意外"

意外一:getClass()返回的是同一个类

List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // true

不管泛型参数是什么,运行时都是同一个类。

意外二:instanceof用不了

// 编译错误
if (list instanceof List<String>) { }

运行时List<String>和List<Integer>没有区别,所以Java干脆禁止这种写法。只能写list instanceof List。

意外三:不能new T(),不能new T[]

public <T> void create() {
    T obj = new T(); // 编译错误
    T[] arr = new T[10]; // 编译错误
}

因为运行时不知道T是什么,new不出来。

三、擦除的底层逻辑

为什么Java非要擦除?一句话:为了兼容

Java 5之前没有泛型,ArrayList只能放Object,取出来强制转型。如果泛型在运行时保留类型信息,所有旧代码和新代码的类文件格式就不兼容了。擦除是Java在"引入新特性"和"保持向后兼容"之间选的折中方案。

C#不走这条路——它的泛型在运行时完全保留,List<String>和List<Integer>运行时是不同类型。代价是新语言、新框架可以这么设计,但Java有20年的存量代码要背。

四、擦除怎么绕过去?

理解擦除不是为了抱怨,而是为了知道怎么绕过它。

绕过1:用Class<T>传类型信息

public <T> T create(Class<T> clazz) throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}

把类型参数作为参数传进去,运行时就能用了。

绕过2:用Super Type Token

Type type = new TypeToken<List<String>>(){}.getType();

利用匿名内部类在编译时保留泛型信息,Spring、Gson、Jackson都用这个方式获取运行时泛型类型。

绕过3:桥方法

class Parent { public Object get() { return "hello"; } }
class Child extends Parent { 
    @Override 
    public String get() { return "world"; } 
}

编译器会生成一个桥方法,让擦除后的代码还能正确多态。这是擦除的一个附带产物。

五、面试能怎么答这道题?

现在再被问到开头那道题,可以这样回答:

"泛型在Java里是通过类型擦除实现的。ArrayList<String>和ArrayList<Integer>的泛型参数只在编译期存在,编译器用它做类型检查,编译成字节码后这些信息就被擦除了。所以运行时它们的Class对象都是同一个ArrayList.class。这是Java为了兼容老版本做的权衡设计。"

补充两句效果更好:"C#的泛型是运行时保留的,但Java选择了擦除,所以泛型在某些场景(比如反射)下有局限性,需要用TypeToken或显式传Class来绕过。"

这个问题本身不难,但能答出"擦除机制+兼容性原因+绕过方式"三个层次,说明对Java泛型的理解已经超过大多数人了。