问题
之前学习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: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/util/ArrayList
8: dup
9: invokespecial #3 // Method java/util/ArrayList."<init>":()V
12: putfield #4 // Field doubles:Ljava/util/List;
15: return
LineNumberTable:
line 6: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcom/eawaun/generic/MyTest;
public static void test(java.util.List<java.lang.Float>);
descriptor: (Ljava/util/List;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 floats Ljava/util/List;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 1 0 floats Ljava/util/List<Ljava/lang/Float;>;
Signature: #24 // (Ljava/util/List<Ljava/lang/Float;>;)V
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 15: 0
line 16: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 list Ljava/util/List;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 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去获取。