众所周知,在Java中由于反射的存在使其可以成为介于Python和C++之间的一直半自动的语言。反射可以强大到在运行时获取类的各种属性,并进行操作。但是在Java中泛型的实现其实是伪泛型,即在编译结束后会擦除实际的泛型类型,最终导致所有地方其实都是Object类型。那么当泛型遇上反射,还能否获取实际类型呢?
目录:
Java中的反射真的可以获取泛型属性吗
首先要说的是,本文建立在你已经对Java中的泛型和反射具有一定的了解的基础上进行讲解,而不会讲解反射的细节。
关于反射,可以参考我之前的文章:Java反射基础总结
先说结论:可以获取
下面给出获取的过程:
获取类属性的泛型类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class GeneralTypeTest {
private List<GeneralTypeObject> list;
private List<?> list2;
public static void main(String[] args) throws Exception { System.out.println(GeneralTypeTest.class.getDeclaredField("list").getGenericType()); System.out.println(GeneralTypeTest.class.getDeclaredField("list2").getGenericType());
GeneralTypeTest generalTypeTest = new GeneralTypeTest(); generalTypeTest.list2 = new ArrayList<>(); } private static class GeneralTypeObject { } }
|
结果如下:
1 2
| java.util.List<GeneralTypeTest$GeneralTypeObject> java.util.List<?>
|
可以看出,通过反射还是很容易获取属性的泛型类型的,只需要通过getGenericType方法即可!
下面再来看看获取方法的参数中的泛型属性
获取方法参数的泛型类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List;
public class GeneralTypeTest {
private List<GeneralTypeObject> list;
private List<?> list2;
public static void main(String[] args) throws Exception { GeneralTypeTest generalTypeTest = new GeneralTypeTest(); generalTypeTest.list = new ArrayList<>(); generalTypeTest.list2 = new ArrayList<>();
generalTypeTest.list.add(new GeneralTypeObject()); generalTypeTest.list.add(new GeneralTypeObject());
generalTypeTest.show(generalTypeTest.list); generalTypeTest.show(generalTypeTest.list2); }
private void show(List<?> list) throws NoSuchMethodException { System.out.println("----------- show method: ---------"); System.out.println(GeneralTypeTest.class.getDeclaredMethods()[0].getParameterTypes()[0].getTypeName()); System.out.println(list); }
private static class GeneralTypeObject {} }
|
show方法的参数中定义了泛型List<?> list接收参数,在show方法中,通过getDeclaredMethods方法获取GeneralTypeTest类中的show方法,然后通过getParameterTypes获取方法参数的类型。
最终输出如下:
1 2 3 4 5 6
| ----------- show method: --------- java.util.List [GeneralTypeTest$GeneralTypeObject@5a39699c, GeneralTypeTest$GeneralTypeObject@3cb5cdba] ----------- show method: --------- java.util.List []
|
可见针对不论List<GeneralTypeObject>还是List<?>的传参,最终由于Java泛型中的类型擦除,最后都导致输出了java.util.List
真的是这样吗?显然不是的!
注意:在show方法中,正确输出了GeneralTypeTest$GeneralTypeObject@5a39699c!说明在JVM中实际上是知道你的泛型类型的!
只是我们获取泛型的姿势不太对罢了!
获取方法参数的泛型类型-续
修改show方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| private void show(List<?> list) throws NoSuchMethodException { System.out.println("----------- show method: ---------"); Method method = GeneralTypeTest.class.getDeclaredMethods()[0];
System.out.println("Method 1:"); System.out.println(method.getParameterTypes()[0].getTypeName());
System.out.println("Method 2:"); Type[] parameterTypes = method.getGenericParameterTypes(); for (Type type : parameterTypes) { System.out.println(type); if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; System.out.println(parameterizedType.getRawType()); Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); for (Type type2 : actualTypeArguments) { System.out.println(type2); } } } System.out.print("List Content: "); System.out.println(list); }
|
在修改的show方法中,首先通过getDeclaredMethods()方法获取到了show方法,然后通过getGenericParameterTypes()方法获取到了包括泛型的方法参数!
之后遍历方法参数数组,判断参数是否是泛型类型(type instanceof ParameterizedType),如果当前参数类型是泛型类型,则强转为ParameterizedType,然后通过getActualTypeArguments()方法获取声明的泛型类型(注意:是声明的泛型类型!)
最终输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ----------- show method: --------- Method 1: java.util.List Method 2: java.util.List<?> interface java.util.List ? List Content: [GeneralTypeTest$GeneralTypeObject@30c7da1e, GeneralTypeTest$GeneralTypeObject@5b464ce8] ----------- show method: --------- Method 1: java.util.List Method 2: java.util.List<?> interface java.util.List ? List Content: []
|
从输出结果可以看出:使用这种方法获得的泛型类型是声明时的泛型类型,而不是运行时的泛型类型
如果把泛型声明的?改为GeneralTypeObject对于list有:
1 2 3 4 5 6 7 8
| ----------- show method: --------- Method 1: java.util.List Method 2: java.util.List<GeneralTypeTest$GeneralTypeObject> interface java.util.List class GeneralTypeTest$GeneralTypeObject List Content: [GeneralTypeTest$GeneralTypeObject@36d64342, GeneralTypeTest$GeneralTypeObject@39ba5a14]
|
即获得的泛型属性是声明时的类型!
但是这里就有一个疑问了,为什么通过反射获取的是声明时的泛型类型,但是在调用println方法时,输出的不是Object,而是对应的类型呢?
这就涉及到Java中的编译优化了!
Java早期编译优化
泛型是JDK 1.5的一项新增特性,其本质即是上面提到的Parametersized Type(参数化类型)。
在JDK 1.5以前,由于没有泛型,所以在使用HashMap的get方法时,返回的都是Object对象,这时由于Object可以是任何类型,所以就只有程序员和运行期的JVM才知道这个Object到底是什么类型。而在编译期间,编译器是无法检测这个Object强转是否成功,只能靠程序员来保证强转的正确性。
在这种情况下,许多在编译期就可以解决的问题,被转移到了ClassCastException这类运行时异常!
为了解决这个问题,泛型应运而生!
在Java早期编译优化时,的确会将源码中的泛型擦拭,比如对于以下两个类型:
1 2
| ArrayList<Integer> ArrayList<String>
|
在Java中,就被认为是同一个类型。
这与C#中的泛型有着本质的区别!
在C#中,源代码在编译之后不同的泛型类型会被不同的占位符替代,而Java全部被擦除!
在下面的例子中:
1 2 3
| Map<String, String> map = new HashMap<>(); map.put("Hello", "H"); System.out.println(map.get("Hello"));
|
如果把这段代码反编译之后,会发现泛型声明都消失了(IDEA还是很贴心的帮你加上了~)!
这段代码更像下面:
1 2 3
| Map map = new HashMap(); map.put("Hello", "H"); System.out.println((String)map.get("Hello"));
|
即:擦除了泛型声明,并把所有的结果做了强转转换!
所以本质上:Java的泛型是一种伪泛型,即前期编译时的语法糖,帮助你解决了大部分运行时才能判断的类型转换异常而已!
也正因为如此,在下面的重载场景下,其实是无法通过编译的:
1 2
| private static void method(List<String> list) {} private static void method(List<Integer> list) {}
|
但是在Sun JDK 1.6中,下面的两个方法是可以通过编译的:
1 2 3 4 5 6 7
| private static String method(List<String> list) { return "123"; }
private static int method(List<Integer> list) { return 1; }
|
这是由于:
在编译期,这两个方法对于编译器来说(未擦除之前)显然是两个参数不同的方法,但是当擦除之后,在同一个Class文件中出现了同一个方法签名(返回值、方法名、方法参数)所以被拒绝;
而修改了返回类型之后,即可共存于同一个Class文件!
(这是非常危险的事情,很可能导致逻辑错误!)
需要注意的是,尽管在前期编译优化中,Java做了泛型擦除,但是还是保留了元数据。
这也是为什么通过泛型可以获取到源代码中声明的泛型类型,而在println中可以获取到运行时类型的原因:
源代码中声明的泛型类型被保存在元数据中,而在泛型擦除的地方(println方法调用时)添加了强转转换!
到底是不是这样呢?我们来看看编译后的Class文件。
查看Class文件
首先通过javac命令编译.java源文件:
1
| javac GeneralTypeTest.java
|
然后通过javap -verbose命令查看编译的.class文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| $ javap -verbose GeneralTypeTest.class Classfile /home/zk/workspace/test/src/main/java/GeneralTypeTest.class Last modified 2020年3月25日; size 2088 bytes MD5 checksum a315127f7f34585e040b1af1501dee6c Compiled from "GeneralTypeTest.java" public class GeneralTypeTest minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: super_class: interfaces: 0, fields: 2, methods: 3, attributes: 3 Constant pool: ...... { public GeneralTypeTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial 4: return LineNumberTable: line 7: 0
public static void main(java.lang.String[]) throws java.lang.Exception; descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: new 3: dup 4: invokespecial 7: astore_1 8: aload_1 9: new 12: dup 13: invokespecial 16: putfield 19: aload_1 20: new 23: dup 24: invokespecial 27: putfield 30: aload_1 31: getfield ...... LineNumberTable: line 14: 0 line 15: 8 line 16: 19 line 18: 30 line 19: 47 line 24: 64 line 25: 72 line 26: 80 Exceptions: throws java.lang.Exception } SourceFile: "GeneralTypeTest.java" NestMembers: GeneralTypeTest$GeneralTypeObject
|
补充:
也可以通过vim打开.class文件,并通过使用命令:%!xxd,即可转变为16进制显示
在Constant pool中的确可以看到泛型的内容,如:#33 = Utf8 Ljava/util/List<LGeneralTypeTest$GeneralTypeObject;>;
这也是我们通过反射获取的类型,至于能否通过反射获取真正的运行时类型,我的答案是:不能!
这也是通过Java中泛型的特性推断出来的。
如果小伙伴们有可以通过反射获取真正的运行时类型的方法,欢迎下方评论留言或私信我。
另外如果文中有错误,也请指出!
如果觉得文章写的不错, 可以关注微信公众号: Coder张小凯
内容和博客同步更新~