问题
之前学习Java泛型的时候会有这个疑问,既然泛型存在类型擦除的特性,为什么在运行时还可以拿到泛型类型信息,两者之间是有什么关系?
猜测
这里我们先理解清楚什么是泛型类型擦除,所谓泛型类型擦除,指的是我们使用泛型的代码在编译运行后,泛型都会被忽略掉,比如
List<String> stringList = new ArrayList<>();List<Integer> integerList = new ArrayList<>();System.out.println(stringList.getClass() == integerList.getClass()); //结果为true
两个List实际上是相同的类型,可以存放任何类型的值,而泛型的作用只是在编译时去检查插入类型是不是要求的类型。
而为什么运行时能拿到泛型信息呢?经过一番查找假设,猜测这些信息是存放在class文件中,于是查看了class文件定义,发现在class文件结构中,就定义了一块区域存放泛型类型(见Signature Attribute)。
JVM对于class上的泛型信息会进行保留,需要用到的时候可以通过反射等机制解析class内容得到,因此擦除不代表就不会保留泛型信息。
对于class上定义的泛型类型,在编译时会放到class文件中,这些泛型类型可以在运行时获取到。注意这里指的是类结构中定义的泛型类型,包括类本身,成员变量,方法签名等,而对于局部变量在编译时会保存到LocalVariableTypeTable中,但运行时JDK并没有提供API来获取。
求证
接下来我们通过一个例子来验证我们的结论。
interface MyParent<T> {}public class MyTest implements MyParent<Boolean> {List<Double> doubles = new ArrayList<>();public static void test(List<Float> floats) {}public static void main(String[] args) {List<Integer> list = new ArrayList<>();}}
结合我们上面的结论,可以先得出我们在运行时可以拿到MyParent<Boolean>、List<Double>、List<Float>,而List<Integer>拿不到的结论。
我们通过javap -verbose MyTest.class反编译class文件得到如下结果(部分结果省略)
public class com.eawaun.generic.MyTest extends java.lang.Object implements com.eawaun.generic.MyParent<java.lang.Boolean>{java.util.List<java.lang.Double> doubles;descriptor: Ljava/util/List;flags:Signature: #11 // Ljava/util/List<Ljava/lang/Double;>;public com.eawaun.generic.MyTest();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: new #2 // class java/util/ArrayList8: dup9: invokespecial #3 // Method java/util/ArrayList."<init>":()V12: putfield #4 // Field doubles:Ljava/util/List;15: returnLineNumberTable:line 6: 0line 8: 4LocalVariableTable:Start Length Slot Name Signature0 16 0 this Lcom/eawaun/generic/MyTest;public static void test(java.util.List<java.lang.Float>);descriptor: (Ljava/util/List;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 12: 0LocalVariableTable:Start Length Slot Name Signature0 1 0 floats Ljava/util/List;LocalVariableTypeTable:Start Length Slot Name Signature0 1 0 floats Ljava/util/List<Ljava/lang/Float;>;Signature: #24 // (Ljava/util/List<Ljava/lang/Float;>;)Vpublic static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=2, args_size=10: new #2 // class java/util/ArrayList3: dup4: invokespecial #3 // Method java/util/ArrayList."<init>":()V7: astore_18: returnLineNumberTable:line 15: 0line 16: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;8 1 1 list Ljava/util/List;LocalVariableTypeTable:Start Length Slot Name Signature8 1 1 list Ljava/util/List<Ljava/lang/Integer;>;}
在上面结果中,可以看到有”Signature“属性,当中会记录对应的泛型信息。对于List<Integer>可以在LocalVariableTypeTable中看到,证明class文件中有记录这个信息,但在JVMS(Java Virtual Machine Specification)中是这么介绍LocalVariableTypeTable的:
It may be used by debuggers to determine the value of a given local variable during the execution of a method.
也就是说这个数据结构是用来给调试器用的,这里个人猜测JDK认为局部变量的泛型信息并没有通过class获取的必要,因为在方法中就直接可以拿到了,所以没有提供方法去获取。
结论
综上,泛型类型擦除与是否能拿到泛型信息并没有关系。泛型类型擦除,指的是我们使用泛型的代码在编译运行后,泛型都会被忽略掉。
但忽略不代表不保留,实际上class文件会记录下泛型信息,可通过提供的API去获取,但其中局部变量的泛型信息JDK并没有提供API去获取。
