JVM的编译时多态与类型擦除

众所周知,java代码在编译过程中会进行类型擦除,类型擦除后泛型信息会丢失。可是,为什么在反射中还可以通过ParameterizedType的getActualTypeArguments方法来获得泛型信息呢。

首先,按照网上的说法,下面这两句所生成的字节码应该是一样的。但事实上,可以发现,他们所生成的字节码是不同的,区别就在Signature和LocalVariableTable中,这里标明了泛型的实际类型。

[java]
public static void print(Set<Integer> c) { } // LocalVariableTable中为Ljava/util/Set<Ljava/lang/Integer;>
public static void print(Set<String> c) { } // LocalVariableTable中为Ljava/util/Set<Ljava/lang/String;>
[/java]

看上去,泛型信息还在,可是为什么这两句放在一起会报错呢?

因为java采用的是编译时多态。java代码在编译过程中会尝试匹配所有同名方法,并且找到唯一符合条件的方法,然后将其签名写入字节码中,通过其签名来调用该方法。因此java中方法名可以相同,但是调用该方法的签名决不能相同
在此例中上面两个方法生成的字节码虽然不同,但是在调用时所用的签名都是”print:(Ljava/util/List;)V”,因而无法区分这两个方法,所以不能多态。
正是因为编译时多态这种编译时分析确定签名,运行时根据签名直接调用的方式。导致下面这两句是完全可以执行的,原因就是虽然实参与形参不一致,但是实参与形参在忽略泛型后是一样的。
[java]
// 注意,这仅仅是个例子,由于编译器的检查功能所以这两句是无法编译通过的。因此需要通过反射的方式来实现。
// 当然,最简单的方式是将这两句分到两个类中用一些trick将两个class分别编译后直接运行,你可以发现代码是可以正常执行的,虽然直接编译它一定会报错。
print(new Set<Integer>());
public static void print(Set<String> c) { }
[/java]

由此可见,java中泛型擦除的确是存在的,运行时的基本可以认为泛型已经被完全擦除。但是,为什么ParameterizedType还是可以取得泛型信息呢?
答案是字节码。要知道,反射与JVM正常的运行是不同的,反射可以直接分析字节码,而字节码中有该类的签名以及变量的签名,从而可以分析出类或变量的泛型信息。

最后,通过查看字节码的反汇编结果可知以下情况中的泛型是可以通过反射获取的。
1:函数返回值中的泛型
2:函数参数中的泛型
3:类的field中的泛型
4:函数中局部变量的泛型「存于LocalVariableTypeTable中,仅用于调试,发布模式下不存在」