泛型擦除的那些事
泛型简介
泛型大家一定再熟悉不过了,java 中的集合类:ArrayList
,HashMap
...
或者是,为了避免重复的模板代码,自定义模板方法:
public class ScheduleTemplate<T> {
public T doSchedule(T name){
//some code
return name;
}
}
在使用的时候,直接在尖括号内< >
传入类型参数,就可以避免很多烦人的强制类型转换。
ScheduleTemplate<String> scheduleTemplate = new ScheduleTemplate<>();
String name = scheduleTemplate.doSchedule("aaa");
本文的重点在于泛型擦除,泛型的使用不再过多介绍。
泛型擦除
证明存在泛型擦除
还是使用上面的 ScheduleTemplate
类,这次我们使用 javap
看一下。
9: ldc #4 // String aaa
11: invokevirtual #5 // Method git/frank/generics/ScheduleTemplate.doSchedule:(Ljava/lang/Object;)Ljava/lang/Object;
14: checkcast #6 // class java/lang/String
17: astore_2
看的出来,在从源文件解释为class文件时,已经把泛型信息去掉了,取而代之的是强制类型转换 checkcast
。
再或者,我们可以通过运行时来看:
ScheduleTemplate<String> strTemplate = new ScheduleTemplate<>();
ScheduleTemplate<Integer> intTemplate = new ScheduleTemplate<>();
System.out.println(strTemplate.getClass() == intTemplate.getClass());
java 为什么要泛型擦除
我们先来看一下 java 中对象的布局:
如果没有泛型擦除的话
如果不做泛型擦除的话,使用上面的例子,应该是下面这个样子:
回想一下,你有多依赖 java 提供的集合类型。
要不是有泛型擦除机制,方法区中将会充斥着各种集合对应的类型数据,例如:ArrayList<String>
,ArrayList<Integer>
,ArrayList<XXX>
,HashMap<String,XXX>
,这样的话,方法区会被大量的几乎无用的类型信息填满。
如何"避免"泛型擦除
定义擦除上限
还是开头的例子,这次我这样定义ScheduleTemplate
:
public class ScheduleTemplate<T extends Number> {
public T doSchedule(T name){
//some code
return name;
}
}
泛型定义中限制了必须是Number
类型,所以我们的测试代码修改成这样:
ScheduleTemplate<Long> longTemplate = new ScheduleTemplate<>();
Long a = longTemplate.doSchedule(1L);
使用 javap 再看一下,这次泛型只擦除到了 Number
,而没有一路到底 Object
。
10: invokestatic #4 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
13: invokevirtual #5 // Method git/frank/generics/ScheduleTemplate.doSchedule:(Ljava/lang/Number;)Ljava/lang/Number;
16: checkcast #6 // class java/lang/Long
当然在运行时也是一样可以获取到 Number
类型的。
使用继承避免“擦除”
上面我们说过,泛型擦除的出现是为了防止出现过多的类信息而设计的。
如果我们想避开泛型擦除的话,让虚拟机额外给我创建一个类信息就可以了。最简单的办法,就是使用继承。
还是使用上面的例子,这次我继承他创建了类 StrScheduleTemplateImpl
。
public class StrScheduleTemplateImpl extends ScheduleTemplate<String> {
@Override
public String doSchedule(String name) {
return super.doSchedule(name);
}
}
现在,我们再使用 javap
看一下。
9: ldc #4 // String aaa
11: invokevirtual #5 // Method git/frank/generics/impl/StrScheduleTemplateImpl.doSchedule:(Ljava/lang/String;)Ljava/lang/String;
14: astore_2
同样的,在运行时也可以获取到类型信息。
在实际使用中,大多使用匿名内部类的方式:
ScheduleTemplate scheduleTemplate = new ScheduleTemplate<String>(){};
这种使用内部类的泛型传递方式在框架中应用非常广泛,大家最熟悉的应该就是 fastJson 中的 TypeReference
吧~
总结
在 java 中,在源代码编译为 class 文件后,泛型信息就已经被抹除掉了。
虽然泛型擦除机制被很多人诟病,但泛型擦除可以大大减少在运行时 JVM 被迫保存过多的类型信息。
如果,你真的需要在运行时获取到泛型,可以通过匿名内部类的方式构造对象,使其可以将泛型保留至运行时。