如何实现Type类型层次结构的类型?

14
当泛型被添加到1.5中时,java.lang.reflect添加了一个Type接口和各种子类型来表示类型。Class类被修改以实现Type来适应1.5之前的类型。从1.5开始,Type子类型可用于新的泛型类型。
这一切都很好。有点尴尬,因为Type必须向下转换才能做任何有用的事情,但通过试验、错误、调整和(自动)测试是可行的。只是在实现时会遇到问题...
如何实现equals和hashCode方法呢?ParameterizedType作为Type的子类型,API描述说:
类的实例必须实现一个equals()方法,该方法将等同于共享相同的泛型类型声明并具有相等类型参数的任何两个实例。
(我想这意味着getActualTypeArguments和getRawType,但不包括getOwnerType?)
我们知道根据java.lang.Object的通用契约,也必须实现hashCode方法,但似乎没有规定该方法应该产生什么值。
除了Class有每个值的不同实例之外,Type的其他子类型似乎都没有提到equals或hashCode。
那么我在equals和hashCode中要放什么呢?
(如果你想知道,我正在尝试用实际类型替换类型参数。因此,如果我在运行时知道TypeVariable T是Class String,那么我就想替换Type,那么List就变成List,T[]就变成String[],List[](可能发生!)就变成List[]等)
或者我必须创建自己的并行类型层次结构(没有复制Type的法律原因),有库吗?
编辑:已经有一些人问我为什么需要这个。确实,为什么要查看泛型类型信息呢?
我开始使用非通用的类/接口类型。(如果您想要参数化类型,例如 List<String>,则可以始终添加一层间接性与新类。) 然后我遵循字段或方法。它们可能引用参数化类型。只要它们没有使用通配符,当面对类似于 T 的情况时,我仍然可以确定实际的静态类型。
通过这种方式,我可以使用高质量的静态类型完成所有工作,没有任何 instanceof 动态类型检查。
在我的情况下,具体的用法是序列化。但它也适用于反射的任何其他合理用途,例如测试。
下面是我用于替换的代码的当前状态。 typeMap 是一个 Map<String,Type>。以“原样”快照的形式存在。没有进行任何整理 (如果您不相信,请抛出 null;)。
   Type substitute(Type type) {
      if (type instanceof TypeVariable<?>) {
         Type actualType = typeMap.get(((TypeVariable<?>)type).getName());
         if (actualType instanceof TypeVariable<?>) { throw null; }
         if (actualType == null) {
            throw new IllegalArgumentException("Type variable not found");
         } else if (actualType instanceof TypeVariable<?>) {
            throw new IllegalArgumentException("TypeVariable shouldn't substitute for a TypeVariable");
         } else {
            return actualType;
         }
      } else if (type instanceof ParameterizedType) {
         ParameterizedType parameterizedType = (ParameterizedType)type;
         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
         int len = actualTypeArguments.length;
         Type[] actualActualTypeArguments = new Type[len];
         for (int i=0; i<len; ++i) {
            actualActualTypeArguments[i] = substitute(actualTypeArguments[i]);
         }
         // This will always be a Class, wont it? No higher-kinded types here, thank you very much.
         Type actualRawType = substitute(parameterizedType.getRawType());
         Type actualOwnerType = substitute(parameterizedType.getOwnerType());
         return new ParameterizedType() {
            public Type[] getActualTypeArguments() {
               return actualActualTypeArguments.clone();
            }
            public Type getRawType() {
               return actualRawType;
            }
            public Type getOwnerType() {
               return actualOwnerType;
            }
            // Interface description requires equals method.
            @Override public boolean equals(Object obj) {
               if (!(obj instanceof ParameterizedType)) {
                  return false;
               }
               ParameterizedType other = (ParameterizedType)obj;
               return
                   Arrays.equals(this.getActualTypeArguments(), other.getActualTypeArguments()) &&
                   this.getOwnerType().equals(other.getOwnerType()) &&
                   this.getRawType().equals(other.getRawType());
            }
         };
      } else if (type instanceof GenericArrayType) {
         GenericArrayType genericArrayType = (GenericArrayType)type;
         Type componentType = genericArrayType.getGenericComponentType();
         Type actualComponentType = substitute(componentType);
         if (actualComponentType instanceof TypeVariable<?>) { throw null; }
         return new GenericArrayType() {
            // !! getTypeName? toString? equals? hashCode?
            public Type getGenericComponentType() {
               return actualComponentType;
            }
            // Apparently don't have to provide an equals, but we do need to.
            @Override public boolean equals(Object obj) {
               if (!(obj instanceof GenericArrayType)) {
                  return false;
               }
               GenericArrayType other = (GenericArrayType)obj;
               return
                   this.getGenericComponentType().equals(other.getGenericComponentType());
            }
         };
      } else {
         return type;
      }
   }

为什么需要添加equals/hashCode方法?您确定内置类型不正确吗? - Peter Lawrey
1
从源代码中读取实现似乎具有这些方法。您可以尝试创建自己的系统,但考虑到可能永远不需要它,这似乎是很多工作。一个简单的解决方案是比较 toString() 的结果。 - Peter Lawrey
创建一个ClassLoader来加载具有所需类型的类并反射它,虽然如果存在多个类加载器,则该方法无法正常工作。 - Tom Hawtin - tackline
这个主题非常有趣;你有任何具体的例子吗? - Bsquare ℬℬ
1
此外,递归类型要特别小心,即 <U extends Comparable<U>> - fps
显示剩余15条评论
2个回答

6

我已经用不尽如意的方法解决了这个问题10年了。首先是使用Guice的MoreTypes.java,然后复制并修改Gson的GsonTypes.java,最后在Moshi的Util.java中再次进行。

Moshi有我最好的方法,这并不意味着它很好。

您不能调用equals()来比较任意Type实现并期望其正常工作。

这是因为Java Types API提供了多种不兼容的方式来建模简单类的数组。您可以将Date[]作为Class<Date[]>GenericArrayType创建,其组件类型为Date。我认为您会从类型为Date[]的字段的反射中获得前者,而从类型为List<Date[]>的字段的反射中获得后者。

哈希码未指定。

我还参与了Android使用的这些类的实现。早期版本的Android具有不同于Java的哈希码,但您在今天找到的所有内容都使用与Java相同的哈希码。

toString方法不好

如果您在错误消息中使用类型,那么必须编写特殊代码以使其漂亮地打印,这很糟糕。

复制粘贴并感到悲伤

我的建议是不要使用equals() + hashCode()来处理未知的Type实现。使用一个规范化函数将其转换为特定的已知实现,并仅在您控制的实现之间进行比较。


4

这是一个依赖于Sun API和反射的小实验(也就是说,它使用反射来处理实现反射的类):

import java.lang.Class;
import java.lang.reflect.*;
import java.util.Arrays;
import sun.reflect.generics.reflectiveObjects.*;

class Types {

  private static Constructor<ParameterizedTypeImpl> PARAMETERIZED_TYPE_CONS =
    ((Constructor<ParameterizedTypeImpl>)
      ParameterizedTypeImpl
      .class
      .getDeclaredConstructors()
      [0]
    );

  static {
      PARAMETERIZED_TYPE_CONS.setAccessible(true);
  }

  /** 
   * Helper method for invocation of the 
   *`ParameterizedTypeImpl` constructor. 
   */
  public static ParameterizedType parameterizedType(
    Class<?> raw,
    Type[] paramTypes,
    Type owner
  ) {
    try {
      return PARAMETERIZED_TYPE_CONS.newInstance(raw, paramTypes, owner);
    } catch (Exception e) {
      throw new Error("TODO: better error handling", e);
    }
  }

  // (similarly for `GenericArrayType`, `WildcardType` etc.)

  /** Substitution of type variables. */
  public static Type substituteTypeVariable(
    final Type inType,
    final TypeVariable<?> variable,
    final Type replaceBy
  ) {
    if (inType instanceof TypeVariable<?>) {
      return replaceBy;
    } else if (inType instanceof ParameterizedType) {
      ParameterizedType pt = (ParameterizedType) inType;
      return parameterizedType(
        ((Class<?>) pt.getRawType()),
        Arrays.stream(pt.getActualTypeArguments())
          .map((Type x) -> substituteTypeVariable(x, variable, replaceBy))
          .toArray(Type[]::new),
        pt.getOwnerType()
      );
    } else {
      throw new Error("TODO: all other cases");
    }
  }

  // example
  public static void main(String[] args) throws InstantiationException {

    // type in which we will replace a variable is `List<E>`
    Type t = 
      java.util.LinkedList
      .class
      .getGenericInterfaces()
      [0];

    // this is the variable `E` (hopefully, stability not guaranteed)
    TypeVariable<?> v = 
      ((Class<?>)
        ((ParameterizedType) t)
        .getRawType()
      )
      .getTypeParameters()
      [0];

    // This should become `List<String>`
    Type s = substituteTypeVariable(t, v, String.class);

    System.out.println("before: " + t);
    System.out.println("after:  " + s);
  }
}

List<E> 中的 E 替换为 String 的结果如下所示:
before: java.util.List<E>
after:  java.util.List<java.lang.String>

主要思路如下:
  • 获取sun.reflect.generics.reflectiveObjects.XyzImpl
  • 获取它们的构造函数,确保它们是accessible
  • 将构造函数.newInstance调用包装在帮助方法中
  • 在一个简单的递归方法substituteTypeVariable中使用帮助方法,通过具体类型替换类型变量重建Type表达式。

我没有实现每种情况,但它应该也适用于更复杂的嵌套类型(因为substituteTypeVariable的递归调用)。

编译器并不真正喜欢这种方法,它会生成关于使用内部Sun API的警告:

警告:ParameterizedTypeImpl是内部专有API,可能会在未来的版本中被删除。

但是,有一个@SuppressWarnings可以解决这个问题。

上面的Java代码是通过翻译以下简短的Scala代码片段得到的(这就是为什么Java代码可能看起来有些奇怪,不完全符合Java语言习惯的原因):
object Types {

  import scala.language.existentials // suppress warnings
  import java.lang.Class
  import java.lang.reflect.{Array => _, _}
  import sun.reflect.generics.reflectiveObjects._

  private val ParameterizedTypeCons = 
    classOf[ParameterizedTypeImpl]
    .getDeclaredConstructors
    .head
    .asInstanceOf[Constructor[ParameterizedTypeImpl]]

  ParameterizedTypeCons.setAccessible(true)

  /** Helper method for invocation of the `ParameterizedTypeImpl` constructor. */
  def parameterizedType(raw: Class[_], paramTypes: Array[Type], owner: Type)
  : ParameterizedType = {
    ParameterizedTypeCons.newInstance(raw, paramTypes, owner)
  }

  // (similarly for `GenericArrayType`, `WildcardType` etc.)

  /** Substitution of type variables. */
  def substituteTypeVariable(
    inType: Type,
    variable: TypeVariable[_],
    replaceBy: Type
  ): Type = {
    inType match {
      case v: TypeVariable[_] => replaceBy
      case pt: ParameterizedType => parameterizedType(
        pt.getRawType.asInstanceOf[Class[_]],
        pt.getActualTypeArguments.map(substituteTypeVariable(_, variable, replaceBy)),
        pt.getOwnerType
      )
      case sthElse => throw new NotImplementedError()
    }
  }

  // example
  def main(args: Array[String]): Unit = {

    // type in which we will replace a variable is `List<E>`
    val t = 
      classOf[java.util.LinkedList[_]]
      .getGenericInterfaces
      .head

    // this is the variable `E` (hopefully, stability not guaranteed)
    val v = 
      t
      .asInstanceOf[ParameterizedType]
      .getRawType
      .asInstanceOf[Class[_]]          // should be `List<E>` with parameter
      .getTypeParameters
      .head                            // should be `E`

    // This should become `List<String>`
    val s = substituteTypeVariable(t, v, classOf[String])

    println("before: " + t)
    println("after:  " + s)
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接