资讯专栏INFORMATION COLUMN

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

linkFly / 1784人阅读

摘要:然而,与普遍印象相反的是,某些情况下在运行时获取到泛型类型信息也是可行的。于是,编译器可以把这部分泛型信息父类的泛型参数是,存储在它的子类的字节码区域中。当使用反射取出中的类型参数时,就必须把这点纳入考量。获取嵌套类的泛型的代码如下

在JDK5引入了泛型特性之后,她迅速地成为Java编程中不可或缺的元素。然而,就跟泛型乍一看似乎非常容易一样,许多开发者也非常容易就迷失在这项特性里。
多数Java开发者都会注意到Java编译器类型擦除实现方式,Type Erasure会导致关于某个Class的所有泛型信息都会在源代码编译时消失掉。在一个Java应用中,可以认为所有的泛型实现类,都共享同一个基础类(注意与继承区分开来)。这是为了兼容JDK5之前的所有JDK版本,就是人们经常说的向后兼容性

向后兼容性
译者注:原文较为琐碎,大致意思是。在JVM整个内存空间中,只会存在一个ArrayList.class
为了能够区分ArrayListArrayList,现在假想的实现方式是在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 = String。于是,Java编译器可以把这部分泛型信息(父类MyGenericClass的泛型参数是String),存储在它的子类MyStringSubClass的字节码区域中。
而且因为这部分泛型信息在被编译后,仅仅被存储在被老版JVM所忽略的字节码区域中,所以这种方式并没有破坏向后兼容性。与此同时,因为T已经被赋值为String,所有的MyStringSubClass类的对象实例仍然共享同一个MyStringSubClass.class

如何获取这块泛型信息?

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

Java API提供了Class.getGenericSuperClass()方法,来取出一个Type类型的实例

如果直接父类的实际类型就是泛型类型的话,那取出的Type类型实例就可以被显示地转换为ParameterizeType

(Type只是一个标记型接口,它里面仅包含一个方法:getTypeName()。所以取出的实例的实际类型会是ParameterizedTypeImpl,但不应直接暴露实际类型,应一直暴露Type接口)。

感谢ParameterizedType接口,现在我们可以直接调用ParameterizeType.getActualTypeArguments()取出又一个Type类型实例数组

父类所有的泛型类型参数都会被包含在这个数组里,并且以被声明的顺序放在数组对应的下标中。

当数组中的类型参数为非泛型类型时,我们就可以简单地把它显示转换为Class

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

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

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只是一个拥有泛型参数的类,并不是MyGenericClass.class的子类。没有显式的子类,就没有地方存储String类型参数。因此上述调用不可避免地会被Java编译器进行类型擦除。如果你已预见到你的项目中会出现这种情况,也想要避免它,一种良好的编程实践是将MyGenericClass声明为abstract

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

链式泛型
class MyGenericClass {}
class MyGenericSubClass extends MyGenericClass {}
class MyStringSubSubClass extends MyGenericSubClass {}

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

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

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

MyStringSubClass.class中存储了MyGenericSubClass --> U = String

MyGenericSubClass.class中仅存储了MyGenericClass --> 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 extends MyOtherClass也是一种完全合法的子类定义。

嵌套类

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

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 = 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是可以声明为static的,这就意味着static情况下,MyGenericInnerClass拥有它自己独享的泛型type命名空间。所以,Java API中所有的TypeVariable接口实现类,都拥有一个属性叫genericDeclaration


如果两个泛型变量被分别定义在不同的类中,那么这两个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]);
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73445.html

相关文章

  • Java泛型类型擦除

    博客地址:Java泛型:类型擦除 前情回顾 Java泛型:泛型类、泛型接口和泛型方法 类型擦除 代码片段一 Class c1 = new ArrayList().getClass(); Class c2 = new ArrayList().getClass(); System.out.println(c1 == c2); /* Output true */ 显然在平时使用中,ArrayList...

    Hanks10100 评论0 收藏0
  • java的集合和泛型的知识点归纳1

    摘要:接口也是集合中的一员,但它与接口有所不同,接口与接口主要用于存储元素,而主要用于迭代访问即遍历中的元素,因此对象也被称为迭代器。迭代器的实现原理我们在之前案例已经完成了遍历集合的整个过程。 【Collection、泛型】 主要内容 Collection集合 迭代器 增强for 泛型 教学目标 [ ] 能够说出集合与数组的区别 [ ] 说出Collection集合的常用功能 [ ]...

    daryl 评论0 收藏0
  • Java随笔-Java泛型的一点学习

    摘要:以上代码编译通过,运行通过引入泛型的同时,也为了兼容之前的类库,开始引入的其实是伪泛型,在生成的字节码中是不包含泛型中的类型信息的。进行类型擦除后,类型参数原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。 Java泛型 Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在...

    codeGoogle 评论0 收藏0
  • Java013-集合

    摘要:集合框架重点理解用于存储数据的容器。集合容器在不断向上抽取过程中。出现了集合体系。,删除将集合中的元素全删除,清空集合。删除集合中指定的对象。注意删除成功,集合的长度会改变。作用用于取集合中的元素。是集合特有的迭代器。是单列集合是双列集合 集合框架(重点理解):用于存储数据的容器。特点:1:对象封装数据,对象多了也需要存储。集合用于存储对象。2:对象的个数确定可以使用数组,但是不确定怎...

    qpal 评论0 收藏0
  • JAVA泛型笔记

    摘要:泛型类泛型类和普通类的区别就是类定义时,在类名后加上泛型声明。泛型类的内部成员方法就可以使用声明的参数类型。 泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type),即所操作的数据类型在定义时被指定为一个参数。当我们使用的时候给这个参数指定不同的对象类型,就可以处理不同的对象。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和...

    n7then 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<