Java反射:获取正确类型的参数化通用超类方法

时间:2017-07-31 14:28:18

标签: java generics reflection

我使用反射来发现类及其超类的方法,以及方法参数和返回值的类型。这大部分都有效,但我遇到了一些特定的通用案例。假设我有一个基类:

package net.redpoint.scratch;
public class Base<E> {
    public E getE() { return null; }
}

一个子类:

package net.redpoint.scratch;
public class Derived extends Base<String> {}

使用反射我可以遍历Derived的方法并获取arg和返回类型(代码省略,但它工作正常)。但是,我也想知道继承的方法。使用下面的代码,我可以非常非常接近。但是获得getE()的正确返回类型是我的选择。我可以得到泛型类型“E”但不是实际类型“java.lang.String”:

package net.redpoint.scratch;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
public class Scratch {
  public static void main(String[] args) throws Exception {
    Class clazz = Class.forName("net.redpoint.scratch.Derived");
    Type superType = clazz.getGenericSuperclass();
    if (superType instanceof ParameterizedType) {
      ParameterizedType superPt = (ParameterizedType)superType;
      Type[] typeArgs = superPt.getActualTypeArguments();
      Type t0 = typeArgs[0];
      // This is "java.lang.String"
      System.out.println(t0.getTypeName());
      Type superRawType = superPt.getRawType();
      if (superRawType instanceof Class) {
        Class superRawClazz = (Class)superRawType;
        for (Method method : superRawClazz.getDeclaredMethods()) {
          if (method.getName().equals("getE")) {
            Type returnType = method.getGenericReturnType();
            if (returnType instanceof TypeVariable) {
              TypeVariable tv = (TypeVariable)returnType;
              // This is "E"
              System.out.println(tv.getName());
              // How do I associate this "E" back to the correct type "java.lang.String"
            }
          }
        }
      }
    }
  }
}

输出结果为:

java.lang.String
E

我的问题是,如何找到“E”实际上是“java.lang.String”?我怀疑它与查找typeArgs []有关,但我不知道如何到达那里。请不要回应,除非你真的使用泛型反射。我看过很多帖子都有“类型擦除可以防止这种情况”的答案,但事实并非如此。

3 个答案:

答案 0 :(得分:4)

我建议使用番石榴。例如:

Type returnType =
    new TypeToken<Derived>() {}
        .resolveType(Base.class.getMethod("getE").getGenericReturnType());

请参阅TypeToken.resolveType(Type)

替代方案非常复杂,但可能。您需要遍历层次结构并将类型变量映射到自己的类型参数。

处理最微不足道的案件将是这样的:

static Type getArgument(TypeVariable<?>   var,
                        ParameterizedType actual) {
    GenericDeclaration decl = var.getGenericDeclaration();
    if (decl != actual.getRawType())
        throw new IllegalArgumentException();
    TypeVariable<?>[] vars = decl.getTypeParameters();
    for (int i = 0; i < vars.length; ++i) {
        if (var == vars[i]) {
            return actual.getActualTypeArguments()[i];
        }
    }
    return null;
}

如果你有类似的东西,那种简单的方法会失败:

abstract class AbstractDerived<T> extends Base<T> {}
class Derived extends AbstractDerived<String> {}

在这种情况下,您需要首先将EBase映射到T AbstractDerived,然后将T映射到String,所以该方法必须递归或迭代超类型。我有一个类似于here的更复杂的例子,但由于多种原因,这个例子仍然是错误的。

您将遇到的另一个障碍是,要返回替换了类型变量的新类型,您需要自己实现ParameterizedTypeGenericArrayTypeWildcardType,否则请使用未记录的sun.reflect课程。

所有这一切都是说你应该只使用已经处理过这些东西的Guava,除非你出于某种原因编写自己的TypeToken之类的东西。

对所有这一点的警告,您似乎已经知道,所有这一切都取决于在extends子句中提供给超类型的实际类型参数(显式或隐式,如匿名)类)。如果您只是执行new Base<Double>(),则无法在运行时恢复类型参数。

答案 1 :(得分:3)

诀窍是,从原始类获取类型参数,并将其与分析返回类型或类型参数时获得的类型变量相关联:

public class Scratch {
  public static void main(String[] args) throws Exception {
    Class clazz = Class.forName("net.redpoint.scratch.Derived");
    Type superType = clazz.getGenericSuperclass();
    if (superType instanceof ParameterizedType) {
      ParameterizedType superPt = (ParameterizedType)superType;
      Type[] typeArgs = superPt.getActualTypeArguments();
      Type superRawType = superPt.getRawType();
      if (superRawType instanceof Class) {
        Class superRawClazz = (Class)superRawType;
        TypeVariable[] typeParms = superRawClazz.getTypeParameters();
        assert typeArgs.length == typeParms.length;
        Map<TypeVariable,Type> typeMap = new HashMap<>();
        for (int i = 0; i < typeArgs.length; i++) {
          typeMap.put(typeParms[i], typeArgs[i]);
        }
        for (Method method : superRawClazz.getDeclaredMethods()) {
          if (method.getName().equals("getE")) {
            Type returnType = method.getGenericReturnType();
            if (returnType instanceof TypeVariable) {
              TypeVariable tv = (TypeVariable)returnType;
              Type specializedType = typeMap.get(tv);
              if (specializedType != null) {
                // This generic parameter was replaced with an actual type
                System.out.println(specializedType.toString());
              } else {
                // This generic parameter is still a variable
                System.out.println(tv.getName());
              }
            }
          }
        }
      }
    }
  }
}

答案 2 :(得分:0)

你的结局是错误的。 getE()的签名已在 base 类中定义。

Derived不会覆盖或以任何方式修改该方法。换句话说:该方法被定义为返回E。扩展Base并提供特定类型的事实不会影响该方法的定义!

因此,你试图进入&#34;改变了#34;返回类型是徒劳的。因为退货类型没有变化。相关 - 请看这里:

    Class superClazz = Class.forName("Derived").getSuperclass();
    for (TypeVariable clz : superClazz.getTypeParameters()) {
        System.out.println(clz);
    }
    Class clazz = Class.forName("Derived");
    Type superType = clazz.getGenericSuperclass();
    if (superType instanceof ParameterizedType) {
        ParameterizedType superPt = (ParameterizedType) superType;
        for (Type t : superPt.getActualTypeArguments()) {
            System.out.println(t.getTypeName());
        }
    }

以上版画:

E
F
java.lang.String
java.lang.Integer

(对于我使用两个参数E,F并将它们替换为String,Integer的重做示例)