Java泛型进阶 – 如何取出泛型类型参数

在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才能发展并构造更多的新特性。

As a consequence

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

assert new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass();

JVM 来说,上述代码等同于

assert new ArrayList.class == ArrayList.class

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

class MyGenericClass<T> { }
class MyStringSubClass extends MyGenericClass<String> { }

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 的情况。

Java泛型进阶 - 如何取出泛型类型参数

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

public static Class<?> findSuperClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) {
    Class<?> subClass = instance.getClass();
    while (subClass != subClass.getSuperClass()) {
        // instance.getClass()不是classOfInterest的子类,
        // 或者,instance就是classOfInterest的直接实例
        subClass = subClass.getSuperClass();
        if (subClass == null)    throw new IllegalArgumentException();
    }
    ParameterizedType pt = (ParameterizedType) subClass.getGenericSuperClass();
    return (Class<?>) pt.getActualTypeArguments()[parameterIndex];
}

public static void main(String[] args) {
    Class<?> genericType = findSuperClassParameterType(new MyStringSubClass(), MyGenericClass.class, 0);
    assert genericType == String.class;
}

然而,请注意到

findSuperClassParamerterType(new MyGenericClass<String>(), MyGenericClass.class, 0)

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

不过,我们还没有解决问题,毕竟我们目前为止还有许多坑没有填。为了说明,想象下列类继承层次:

class MyGenericClass<T> {}
class MyGenericSubClass<U> extends MyGenericClass<U> {}
class MyStringSubSubClass extends MyGenericSubClass<String> {}

如果现在调用

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

仍然会抛出异常。这次又是为什么呢?到目前为止,我们的假设都是 MyGenericClass 的类型参数 T 的相关信息会存储在它的 直接子类 中,结合第一个例子,就是 MyStringSubClass 会将 T 映射成 String 。但凡是总有无赖,现在 MyStringSubSubClassU 映射成 String ,此时 MyGenerciSubClass 仅仅知道 U = TU 甚至都不是一个实际类型,仅仅是 Java TypeVariable类型 的类型变量,如果我们想要解析这种继承关系,就必须解析它们之间所有的依赖关系。

当然还是能做到的:

public static Class<?> findSubClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) {
  Map<Type, Type> typeMap = new HashMap<Type, Type>();
  Class<?> instanceClass = instance.getClass();
  while (classOfInterest != instanceClass.getSuperclass()) {
    extractTypeArguments(typeMap, instanceClass);
    instanceClass = instanceClass.getSuperclass();
    if (instanceClass == null) throw new IllegalArgumentException();
  }
  
  ParameterizedType parameterizedType = (ParameterizedType) instanceClass.getGenericSuperclass();
  Type actualType = parameterizedType.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<Type, Type> typeMap, Class<?> clazz) {
  Type genericSuperclass = clazz.getGenericSuperclass();
  if (!(genericSuperclass instanceof ParameterizedType)) {
    return;
  }
  
  ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
  Type[] typeParameter = ((Class<?>) parameterizedType.getRawType()).getTypeParameters();
  Type[] actualTypeArgument = parameterizedType.getActualTypeArguments();
  for (int i = 0; i < typeParameter.length; i++) {
    if(typeMap.containsKey(actualTypeArgument[i])) {
      actualTypeArgument[i] = typeMap.get(actualTypeArgument[i]);
    }
    typeMap.put(typeParameter[i], actualTypeArgument[i]);
  }
}

原文 

https://segmentfault.com/a/1190000018319217

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Java泛型进阶 – 如何取出泛型类型参数

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址