高级Java泛型:检索泛型类型参数

在JDK5中引入Java泛型之后, Java泛型Swift成为许多Java程序的组成部分。 但是,乍一看似乎很简单的Java泛型,程序员很快就会迷失此功能。

大多数Java程序员都知道Java编译器的类型擦除 。 一般来说,类型擦除意味着有关Java类的所有通用类型信息在其源代码的编译过程中都会丢失。 这是对Java向后兼容性的致敬:Java类的所有通用变体在正在运行的Java应用程序中共享一个表示。 如果ArrayList <String>的实例必须记住其泛型类型为String类型,则它必须将该信息存储在其功能描述中的某个位置,以指示例如List.get实际上返回String类型。 (通过功能描述,我指的是在类的所有实例之间共享的属性。这包括例如方法或字段定义。与功能描述相反,每个实例各自的实例状态存储在其对象表示中。 )因此ArrayList <String>实例的功能描述由其类ArrayList.class表示。 但是,由于ArrayList.class实例与其他实例(也可以是ArrayList <Integer>类型)共享,因此这将需要具有ArrayList.class的两个不同版本。 但是,这种类表示形式的修改对于较旧的JRE来说是无法理解的,因此会破坏Java应用程序的向后兼容性。 因此,以下比较将始终成功:

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

由于这样的比较是在运行时进行的,而该运行时已经擦除了类的通用类型,因此该比较将ArrayList.class == ArrayList.class转化为琐碎的事情。 更具体地说,正在运行的应用程序将确定ArrayList.class等于其自身,并返回true,尽管有String.class!= Integer.class。 这是Java与其他编程语言(例如C ++)的主要区别,也是人们普遍抱怨Java的原因。 (从学术上讲,C ++实际上并不了解泛型类型。相反, C ++提供了与泛型相似的模板 。)

到目前为止,对于许多开发人员而言,这并不是什么新鲜事物。 但是,与流行的看法相反,有时甚至在运行时有时也可以检索通用类型信息。 在解释之前,让我们看一个例子。 为此,我们定义以下两个类:

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

MyGenericClass具有通用类型T的单个参数。MyStringSubClass扩展了该通用类,并将T = String分配为其类型参数。 结果,Java编译器能够在子类 MyStringSubClass的字节码中存储有关超类MyGenericClass的通用参数类型String的信息。 可以在不破坏向后兼容性的情况下实现此修改,因为此信息仅存储在已编译类的字节码的区域中,而旧JRE版本会忽略该信息。 同时,MyStringSubClass的所有实例仍可以共享一个类表示形式,因为为MyStringSubClass的所有实例设置了T = String。

但是,如何获取存储在字节码中的信息呢? Java API提供了Class.getGenericSuperclass方法,该方法可用于接收Type类型的实例。 如果直接超类实际上是通用的,则返回的实例的类型另外为ParameterizedType,并且可以强制转换为该实例。 (类型不过是标记接口 。实际实例将是内部ParameterizedTypeImpl类的实例,但是您应始终将其强制转换为该接口。)由于强制转换为ParameterizedType接口,因此您现在可以调用ParameterizedType.getActualTypeArguments方法检索再次为Type类型的数组。 泛型超类的任何泛型类型参数将包含在此数组中与类型定义相同的索引处。 任何代表非泛型类的Type实例都只是Java类类的实现。 (假设您没有处理返回类型为GenericArrayType的数组。为简单起见,我将在本文中跳过此方案。)

现在,我们可以利用这些知识来编写实用程序函数:

public static Class<?> findSuperClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) {
  Class<?> subClass = instance.getClass();
  while (subClass != subClass.getSuperclass()) {
    // instance.getClass() is no subclass of classOfInterest or instance is a direct instance of classOfInterest
    subClass = subClass.getSuperclass();
    if (subClass == null) throw new IllegalArgumentException();
  }
  ParameterizedType parameterizedType = (ParameterizedType) subClass.getGenericSuperclass();
  return (Class<?>) parameterizedType.getActualTypeArguments()[parameterIndex];
}

该函数将浏览实例的类层次结构,直到将classOfInterest识别为层次结构中的下一个直接子类。 在这种情况下,将使用Class.getGenericSuperclass方法检索此超类。 如上所述,此方法以包装表示形式(ParamererizedType)返回类的超类,该表示形式包含在子类中找到的泛型类型。 这使我们能够成功运行以下应用程序:

Class<?> genericType = findSuperClassParameterType(new MyStringSubClass(), MyGenericClass.class, 0);
assert genericType == String.class;

但是要注意

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

在此实现中将引发异常。 如前所述:通用信息只能在子类的帮助下进行检索。 但是,MyGenericClass <String>不是MyGenericClass.class的子类,而是具有泛型参数的直接实例。 但是没有显式子类,就没有<something> .class表示形式来存储String参数。 因此,这次,在编译过程中无法删除通用类型。 因此,如果打算对类执行此类查询,则最好将MyGenericClass定义为抽象。

但是,由于到目前为止存在许多陷阱,我们仍未解决问题。 为了说明原因,请考虑以下类层次结构:

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。 相反,现在MyStringSubSubClass存储引用U = String,而MyGenericSubClass只知道U =T。但是,U不是实际类,而是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]);
  }
}

上面的代码将通过在映射中跟踪它们来解析任何链接的泛型类型定义。 请注意,由于MyClass <A,B>扩展了MyOtherClass <B,A>定义了完全合法的子类型,因此仅按特定索引检查所有类型定义是不够的。

但是,我们仍然没有完成。 再次,我们将首先看一个示例:

class MyGenericOuterClass<U> {
  public class MyGenericInnerClass<U> { }
}
class MyStringOuterSubClass extends MyGenericOuterClass<String> { }
 
MyStringOuterSubClass.MyGenericInnerClass inner = new MyStringOuterSubClass().new MyGenericInnerClass();

这次通过调用对内部类的反思

findSuperClassParameterType(inner, MyGenericInnerClass.class, 0);

将失败。 乍一看,这似乎是必然的结果。 我们正在同一类的实例上的MyGenericInnerClass中寻找通用参数类型。 如上所述,由于无法在MyGenericInnerClass.class中存储任何泛型类型信息,因此这通常是不可能的。 但是,在这里,我们检查了泛型类的子类型的(非静态)内部类的实例。 MyStringOuterSubClass知道U =字符串。 在考虑MyGenericInnterClass的参数类型时,我们必须考虑到这一点。

现在,这里的事情变得非常棘手。 为了在外部类中找到泛型声明,我们必须首先掌握该外部类。 这可以通过反射和Java编译器增加了一个这样的事实来实现的合成 (这意味着没有源代码表示)字段此$ 0到任何内部类。 可以通过调用Class.getDeclaredField(“ this $ 0”)来检索此字段。 通过获取包含当前内部类的外部类的实例,我们可以自动访问其Java类。 现在,我们可以按照上述步骤进行操作,并在封闭的类中扫描通用定义,然后将其添加到地图中。 但是,MyGenericOuterClass中U的类型变量表示形式将不等于MyGenericInnerClass中U的类型表示形式。 就我们所知,MyGenericInnerClass可以是静态的,并定义自己的通用变量名称空间。 因此,任何表示Java API中通用变量的TypeVariable类型都具有genericDeclaration属性。 如果两个通用变量在不同的类中定义,则TypeVariable表示形式在它们的定义上是不相等的,即使它们在同一个名称空间中共享一个名称,而一个类又是另一个的非静态内部类。

因此,我们必须执行以下操作:

  1. 首先,尝试在内部类超类层次结构中找到泛型类型。 就像处理非嵌套类一样。
  2. 如果无法解析类型:对于(非静态)内部类及其所有外部类,请尽可能完整地解析类型变量。 这可以通过相同的extractTypeArguments算法实现,并且对于每个嵌套类基本​​上为1 .。 我们可以通过检查this $ 0字段是否为内部类定义来获取外部类。
  3. 检查外部类之一是否包含具有相同变量名的通用变量的定义。 在这种情况下,您找到了所需的通用变量的实际类型。

在代码中,如下所示:

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 if (actualType instanceof TypeVariable) {
    return browseNestedTypes(instance, (TypeVariable<?>) actualType);
  } else {
    throw new IllegalArgumentException();
  }
}
 
private static Class<?> browseNestedTypes(Object instance, TypeVariable<?> actualType) {
  Class<?> instanceClass = instance.getClass();
  List<Class<?>> nestedOuterTypes = new LinkedList<Class<?>>();
  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<Type, Type> outerTypeMap = new HashMap<Type, Type>();
      extractTypeArguments(outerTypeMap, outerClass);
      for (Map.Entry<Type, Type> 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) { /* 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;
}

哇,真丑! 但是上面的代码使findSubClassParameterType甚至可以用于嵌套类。 我们可以进一步详细介绍,因为我们还可以找到通用接口,通用方法,字段或数组的类型。 但是,所有此类提取的想法都相同。 如果子类知道其父类的泛型参数,则可以通过反射来获取它们。 否则,由于类型擦除,泛型参数将在运行时丢失而无法挽回。

但是最后,这有什么好处? 对于许多开发人员而言,这传达了执行黑魔法的印象,因此他们宁愿避免编写此类代码。 诚然,通常有更简单的方法来执行这种查询。 我们可以这样定义MyGenericSubclass:

class MyGenericClass<T> {
  private final Class<T> clazz;
  public MyGenericClass(Class<T> clazz) {
    this.clazz = clazz;
  }
  public Class<T> getGenericClass() {
    return clazz;
  }
}

当然,这也很好,甚至是更少的代码。 但是,当您编写供其他开发人员使用的API时,您通常希望它们尽可能的小巧和简单。 (这可以从编写一个大型框架到以两个为一组的形式编写软件。)通过上述实现,您迫使班级用户提供可以通过不同方式检索的冗余信息。 另外,这种方法对于接口(其中您隐式地要求实现类添加相应的构造函数)不太可能有效。 当着眼于Java 8及其功能接口 (也称为闭包或lambda表达式)时,此问题将变得更加重要。 如果您需要通用接口除了提供其功能方法之外还提供getGenericClass方法,则不能再在lambda表达式中使用它们。

PS:在写这篇博客文章时,我破解了此代码,但从未真正测试过,而是通过dupa调试 。 如果您需要这种功能,那么可以使用一个出色的名为gentyref的库来提供上述分析以及更多内容。

参考: 高级Java泛型:My My Java Java博客上,从我们的JCG合作伙伴 Rafael Winterhalter 检索泛型类型参数

翻译自: https://www.javacodegeeks.com/2013/12/advanced-java-generics-retreiving-generic-type-arguments.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值