问题

之前学习Java泛型的时候会有这个疑问,既然泛型存在类型擦除的特性,为什么在运行时还可以拿到泛型类型信息,两者之间是有什么关系?

猜测

这里我们先理解清楚什么是泛型类型擦除,所谓泛型类型擦除,指的是我们使用泛型的代码在编译运行后,泛型都会被忽略掉,比如

  1. List<String> stringList = new ArrayList<>();
  2. List<Integer> integerList = new ArrayList<>();
  3. System.out.println(stringList.getClass() == integerList.getClass()); //结果为true

两个List实际上是相同的类型,可以存放任何类型的值,而泛型的作用只是在编译时去检查插入类型是不是要求的类型。

而为什么运行时能拿到泛型信息呢?经过一番查找假设,猜测这些信息是存放在class文件中,于是查看了class文件定义,发现在class文件结构中,就定义了一块区域存放泛型类型(见Signature Attribute)。
JVM对于class上的泛型信息会进行保留,需要用到的时候可以通过反射等机制解析class内容得到,因此擦除不代表就不会保留泛型信息。

对于class上定义的泛型类型,在编译时会放到class文件中,这些泛型类型可以在运行时获取到。注意这里指的是类结构中定义的泛型类型,包括类本身,成员变量,方法签名等,而对于局部变量在编译时会保存到LocalVariableTypeTable中,但运行时JDK并没有提供API来获取。

求证

接下来我们通过一个例子来验证我们的结论。

  1. interface MyParent<T> {
  2. }
  3. public class MyTest implements MyParent<Boolean> {
  4. List<Double> doubles = new ArrayList<>();
  5. public static void test(List<Float> floats) {
  6. }
  7. public static void main(String[] args) {
  8. List<Integer> list = new ArrayList<>();
  9. }
  10. }

结合我们上面的结论,可以先得出我们在运行时可以拿到MyParent<Boolean>List<Double>List<Float>,而List<Integer>拿不到的结论。
我们通过javap -verbose MyTest.class反编译class文件得到如下结果(部分结果省略)

  1. public class com.eawaun.generic.MyTest extends java.lang.Object implements com.eawaun.generic.MyParent<java.lang.Boolean>
  2. {
  3. java.util.List<java.lang.Double> doubles;
  4. descriptor: Ljava/util/List;
  5. flags:
  6. Signature: #11 // Ljava/util/List<Ljava/lang/Double;>;
  7. public com.eawaun.generic.MyTest();
  8. descriptor: ()V
  9. flags: ACC_PUBLIC
  10. Code:
  11. stack=3, locals=1, args_size=1
  12. 0: aload_0
  13. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  14. 4: aload_0
  15. 5: new #2 // class java/util/ArrayList
  16. 8: dup
  17. 9: invokespecial #3 // Method java/util/ArrayList."<init>":()V
  18. 12: putfield #4 // Field doubles:Ljava/util/List;
  19. 15: return
  20. LineNumberTable:
  21. line 6: 0
  22. line 8: 4
  23. LocalVariableTable:
  24. Start Length Slot Name Signature
  25. 0 16 0 this Lcom/eawaun/generic/MyTest;
  26. public static void test(java.util.List<java.lang.Float>);
  27. descriptor: (Ljava/util/List;)V
  28. flags: ACC_PUBLIC, ACC_STATIC
  29. Code:
  30. stack=0, locals=1, args_size=1
  31. 0: return
  32. LineNumberTable:
  33. line 12: 0
  34. LocalVariableTable:
  35. Start Length Slot Name Signature
  36. 0 1 0 floats Ljava/util/List;
  37. LocalVariableTypeTable:
  38. Start Length Slot Name Signature
  39. 0 1 0 floats Ljava/util/List<Ljava/lang/Float;>;
  40. Signature: #24 // (Ljava/util/List<Ljava/lang/Float;>;)V
  41. public static void main(java.lang.String[]);
  42. descriptor: ([Ljava/lang/String;)V
  43. flags: ACC_PUBLIC, ACC_STATIC
  44. Code:
  45. stack=2, locals=2, args_size=1
  46. 0: new #2 // class java/util/ArrayList
  47. 3: dup
  48. 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
  49. 7: astore_1
  50. 8: return
  51. LineNumberTable:
  52. line 15: 0
  53. line 16: 8
  54. LocalVariableTable:
  55. Start Length Slot Name Signature
  56. 0 9 0 args [Ljava/lang/String;
  57. 8 1 1 list Ljava/util/List;
  58. LocalVariableTypeTable:
  59. Start Length Slot Name Signature
  60. 8 1 1 list Ljava/util/List<Ljava/lang/Integer;>;
  61. }

在上面结果中,可以看到有”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去获取。

参考链接