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泛型的理解已经超过大多数人了。
