博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java泛型进阶 - 如何取出泛型类型参数
阅读量:7250 次
发布时间:2019-06-29

本文共 9564 字,大约阅读时间需要 31 分钟。

在JDK5引入了泛型特性之后,她迅速地成为Java编程中不可或缺的元素。然而,就跟泛型乍一看似乎非常容易一样,许多开发者也非常容易就迷失在这项特性里。

多数Java开发者都会注意到Java编译器类型擦除实现方式,Type Erasure会导致关于某个Class的所有泛型信息都会在源代码编译时消失掉。在一个Java应用中,可以认为所有的泛型实现类,都共享同一个基础类(注意与继承区分开来)。这是为了兼容JDK5之前的所有JDK版本,就是人们经常说的向后兼容性

向后兼容性

译者注:原文较为琐碎,大致意思是。在JVM整个内存空间中,只会存在一个
ArrayList.class
为了能够区分
ArrayList<String>
ArrayList<Integer>,现在假想的实现方式是在
Class文件信息表(函数表+字段表)里添加额外的泛型信息。这个时候JVM的内存空间中就会存在
(假设)ArrayList&String.class
(假设)ArrayList&Integer.class文件。顺着这种情况延续下去的话,就必须要修改JDK5之前所有版本的JVM对
Class文件的识别逻辑,因为它破坏了
JVM内部一个Class只对应唯一一个.class这条规则。这也是人们常说的: 破坏了
向后兼容性

注:参考Python3舍弃掉Python2的例子,也是放弃了对2的兼容,Python3才能发展并构造更多的新特性。

那应该怎么做?

既然Java开发团队选择了兼容JDK5之前的版本,那就不能在JVM里做手脚了。但Java编译器的代码似乎还是可以修改的。于是,Java编译器编译时就会把泛型信息都擦除,所以以下的比较在JVM运行时会永远为真。

assert new ArrayList
().getClass() == new ArrayList
().getClass();

JVM运行时来说,上述代码等同于

assert new ArrayList.class == ArrayList.class

到目前为止,上述内容都是大家所熟知的事情。然而,与普遍印象相反的是,某些情况下在运行时获取到泛型类型信息也是可行的。举个栗子:

class MyGenericClass
{ }class MyStringSubClass extends MyGenericClass
{ }

MyStringSubClass相当于对MyGenericClass<T>做了类型参数赋值T = String。于是,Java编译器可以把这部分泛型信息(父类MyGenericClass的泛型参数是String),存储在它的子类MyStringSubClass的字节码区域中。

而且因为这部分泛型信息在被编译后,仅仅被存储在被老版JVM所忽略的字节码区域中,所以这种方式并没有破坏向后兼容性。与此同时,因为T已经被赋值为String,所有的MyStringSubClass类的对象实例仍然共享同一个MyStringSubClass.class

如何获取这块泛型信息?

应该如何获取到被存储在byte code区域的这块泛型信息呢?

  1. Java API提供了Class.getGenericSuperClass()方法,来取出一个Type类型的实例
  2. 如果直接父类的实际类型就是泛型类型的话,那取出的Type类型实例就可以被显示地转换为ParameterizeType

    (Type只是一个标记型接口,它里面仅包含一个方法:
    getTypeName()。所以取出的实例的实际类型会是
    ParameterizedTypeImpl,但不应直接暴露实际类型,应一直暴露
    Type接口)。
  3. 感谢ParameterizedType接口,现在我们可以直接调用ParameterizeType.getActualTypeArguments()取出又一个Type类型实例数组
  4. 父类所有的泛型类型参数都会被包含在这个数组里,并且以被声明的顺序放在数组对应的下标中。
  5. 当数组中的类型参数为非泛型类型时,我们就可以简单地把它显示转换为Class<?>

    为了保持文章的简洁性,我们跳过了
    GenericArrayType的情况。

clipboard.png

现在我们可以使用以上知识编写一个工具类了:

public static Class
findSuperClassParameterType(Object instance, Class
clazzOfInterest, int parameterIndex) { Class
subClass = instance.getClass(); while (subClass.getSuperclass() != clazzOfInterest) { subClass = subClass.getSuperclass(); if (subClass == null) throw new IllegalArgumentException(); } ParameterizedType pt = (ParameterizedType) (subClass.getGenericSuperclass()); return (Class
) pt.getActualTypeArguments()[parameterIndex];}public static void testCase1() { Class
genericType = findDirectSuperClassParameterType(new MyStringSubClass()); System.out.println(genericType); assert genericType == String.class;}

然而,请注意到

findSuperClassParamerterType(new MyGenericClass
(), MyGenericClass.class, 0)

这样调用会抛出IllegalArgumentException异常。之前说过:泛型信息只有在子类的帮助下才能被取出。然而,MyGenericClass<String>只是一个拥有泛型参数的类,并不是MyGenericClass.class的子类。没有显式的子类,就没有地方存储String类型参数。因此上述调用不可避免地会被Java编译器进行类型擦除。如果你已预见到你的项目中会出现这种情况,也想要避免它,一种良好的编程实践是将MyGenericClass声明为abstract

然而,我们还没有解决问题,毕竟我们目前为止还有许多坑没有填。

链式泛型

class MyGenericClass
{}class MyGenericSubClass
extends MyGenericClass {}class MyStringSubSubClass extends MyGenericSubClass
{}

如下调用,仍然会抛出异常。

findSuperClassParameterType(new MyStringSubClass(), MyGenericClass.class, 0);

clipboard.png

这又是为什么呢?到目前为止我们都在设想:MyGenericClass的类型参数T的相关信息会存储在它的直接子类中。那么上述的类继承关系就有以下逻辑:

  1. MyStringSubClass.class中存储了MyGenericSubClass<U> --> U = String
  2. MyGenericSubClass.class中仅存储了MyGenericClass<T> --> T = U

U并不是一个Class类型,而是TypeVariable类型的类型变量,如果我们想要解析这种继承关系,就必须解析它们之间所有的依赖关系。代码如下:

public static Class
findSubClassParameterType(Object instance, Class
classOfInterest, int parameterIndex) { Map
typeMap = new HashMap<>(); Class
instanceClass = instance.getClass(); while (instanceClass.getSuperclass() != classOfInterest) { extractTypeArguments(typeMap, instanceClass); instanceClass = instanceClass.getSuperclass(); if (instanceClass == null) throw new IllegalArgumentException(); } // System.out.println(typeMap); ParameterizedType pt = (ParameterizedType) instanceClass.getGenericSuperclass(); Type actualType = pt.getActualTypeArguments()[parameterIndex]; if (typeMap.containsKey(actualType)) { actualType = typeMap.get(actualType); } if (actualType instanceof Class) { return (Class
) actualType; } else { throw new IllegalArgumentException(); }}private static void extractTypeArguments(Map
typeMap, Class
clazz) { Type genericSuperclass = clazz.getGenericSuperclass(); if (!(genericSuperclass instanceof ParameterizedType)) { return ; } ParameterizedType pt = (ParameterizedType) genericSuperclass; Type[] typeParameters = ((Class
) pt.getRawType()).getTypeParameters(); Type[] actualTypeArguments = pt.getActualTypeArguments(); for (int i = 0; i < typeParameters.length; i++) { if (typeMap.containsKey(actualTypeArguments[i])) { actualTypeArguments[i] = typeMap.get(actualTypeArguments[i]); } typeMap.put(typeParameters[i], actualTypeArguments[i]); }}

代码中通过一个map可以解析所有链式泛型类型的定义。不过仍然不够完美,毕竟MyClass<A, B> extends MyOtherClass<B, A>也是一种完全合法的子类定义。

嵌套类

好了好了,仍然没有结束:

class MyGenericOuterClass {  public class MyGenericInnerClass { }}class MyStringOuterSubClass extends MyGenericOuterClass
{ } MyStringOuterSubClass.MyGenericInnerClass inner = new MyStringOuterSubClass().new MyGenericInnerClass();

下面这样调用仍然会失败。

findSuperClassParameterType(inner, MyGenericInnerClass.class, 0);

这种失败几乎是可预见的,我们正试图在MyGenericInnerClass的对象实例里面寻找MyGenericInnerClass的泛型信息。就像之前所说,因为MyGenericInnerClass并没有子类,所以从MyGenericInnerClass.class中寻找泛型信息是不可能的,毕竟MyGenericInnerClass.class里面根本就不存在泛型信息。不过在这个例子中,我们检查的是MyStringOuterSubClass中的非static内部类: MyGenericInnerClass的对象实例。那么,MyStringOuterSubClass是知道它的父类MyGennericOuterClass<U> --> U = String。当使用反射取出MyGenericInnerClass中的类型参数时,就必须把这点纳入考量。

现在这件事就变得相当棘手了。

-> 为了取出MyGenericOuterClass的泛型信息
-> 就必须先得到MyGenericOuterClass.class

这依然可以通过反射取得,Java编译器会在内部类MyGenericInnerClass中生成一个synthetic-field: this$0,这个字段可以通过Class.getDeclaredField("this$0")获取到。

> javap -p -v MyGenericOuterClass$MyGenericInnerClass.class......  final cn.local.test.MyGenericOuterClass this$0;    descriptor: Lcn/local/test/MyGenericOuterClass;    flags: ACC_FINAL, ACC_SYNTHETIC...

既然已经有办法可以获取到MyGenericOuterClass.class了,那接下来我们似乎可以直接复用之前的扫描逻辑了。

这里需要注意,
MyGenericOuterClass<U>的U 并不等同于 <MyGenericInnerClass<U>的U
我们可以做以下推理,
MyGenericInnerClass是可以声明为
static的,这就意味着
static情况下,
MyGenericInnerClass拥有它自己独享的
泛型type命名空间。所以,Java API中所有的
TypeVariable接口实现类,都拥有一个属性叫
genericDeclaration
clipboard.png
clipboard.png
如果两个
泛型变量被分别定义在不同的类中,那么这两个
TypeVariable类型变量,从
genericDeclaration的定义上来说就是不相等的。

获取嵌套类的泛型的代码如下:

private static Class
browseNestedTypes(Object instance, TypeVariable
actualType) { Class
instanceClass = instance.getClass(); List
> nestedOuterTypes = new LinkedList
>(); for ( Class
enclosingClass = instanceClass.getEnclosingClass(); enclosingClass != null; enclosingClass = enclosingClass.getEnclosingClass() ) { try { Field this$0 = instanceClass.getDeclaredField("this$0"); Object outerInstance = this$0.get(instance); Class
outerClass = outerInstance.getClass(); nestedOuterTypes.add(outerClass); Map
outerTypeMap = new HashMap<>(); extractTypeArguments(outerTypeMap, outerClass); for (Map.Entry
entry : outerTypeMap.entrySet()) { if (!(entry.getKey() instanceof TypeVariable)) { continue; } TypeVariable
foundType = (TypeVariable
) entry.getKey(); if (foundType.getName().equals(actualType.getName()) && isInnerClass(foundType.getGenericDeclaration(), actualType.getGenericDeclaration())) { if (entry.getValue() instanceof Class) { return (Class
) entry.getValue(); } actualType = (TypeVariable
) entry.getValue(); } } } catch (NoSuchFieldException e) { /* however, this should never happen. */ } catch (IllegalAccessException e) { /* this might happen */ } } throw new IllegalArgumentException();}private static boolean isInnerClass(GenericDeclaration outerDeclaration, GenericDeclaration innerDeclaration) { if (!(outerDeclaration instanceof Class) || !(innerDeclaration instanceof Class)) { throw new IllegalArgumentException(); } Class
outerClass = (Class
) outerDeclaration; Class
innerClass = (Class
) innerDeclaration; while ((innerClass = innerClass.getEnclosingClass()) != null) { if (innerClass == outerClass) { return true; } } return false;}private static void extractTypeArguments(Map
typeMap, Class
clazz) { Type genericSuperclass = clazz.getGenericSuperclass(); if (!(genericSuperclass instanceof ParameterizedType)) { return; } ParameterizedType pt = (ParameterizedType) genericSuperclass; Type[] typeParameters = ((Class
) pt.getRawType()).getTypeParameters(); Type[] actualTypeArguments = pt.getActualTypeArguments(); for (int i = 0; i < typeParameters.length; i++) { if (typeMap.containsKey(actualTypeArguments[i])) { actualTypeArguments[i] = typeMap.get(actualTypeArguments[i]); } typeMap.put(typeParameters[i], actualTypeArguments[i]); }}

转载地址:http://klhbm.baihongyu.com/

你可能感兴趣的文章
const 的用法总结
查看>>
2017企业网盘年终盘点|机遇与挑战并存,寡头显现
查看>>
将linux用在开发环境中
查看>>
在 Cent OS 6.5 中安装桌面环境
查看>>
liquibase判断mysql表字段是否存在
查看>>
透彻理解VLAN技术
查看>>
linux-Centos 7下bond与vlan技术的结合
查看>>
sqoop2安装配置
查看>>
ulimit调优|设置普通用户的ulimit值
查看>>
AGG第九课 agg::rendering_buffer 渲染缓存
查看>>
mysql5.6 的--dump-slave参数的用法
查看>>
rsync同步的实现及其简单源码包的编译安装
查看>>
AGG第三十八课 一些不常用的坐标转换管道
查看>>
实战案例:创建支持SSH服务的镜像
查看>>
Fiddler Web Debugger简单调试头部参数
查看>>
Linux环境下发布项目(Tomcat重新启动)
查看>>
centos7配置svn服务器
查看>>
亮剑:PHP,我的未来不是梦(13)
查看>>
MYSQL主从数据同步
查看>>
javascript数组操作
查看>>