问题描述
为了提供一些背景知识,我正在创建一个小的依赖注入器,但在将方法调用转换回它们的返回类型时遇到了问题。一个最小的例子是:
public class MinimalExample {
public static <T> void invokeMethod(Class<T> aClass) throws ReflectiveOperationException {
Optional<Method> myOptMethod = resolveMethod(aClass);
if (myOptMethod.isPresent()) {
Method myMethod = myOptMethod.get();
Object myInstance = myMethod.invoke(myMethod);
doSomething(myMethod.getReturnType(),myMethod.getReturnType().cast(myInstance));
}
}
private static <T> Optional<Method> resolveMethod(Class<T> aClass) {
return Stream.of(aClass.getmethods())
.filter(aMethod -> Modifier.isstatic(aMethod.getModifiers()))
.filter(aMethod -> aMethod.getParameterCount() == 0)
.findAny();
}
private static <U> void doSomething(Class<U> aClass,U anInstance) {
// E.g. Map aClass to anInstance.
}
}
这里的问题是,doSomething
需要用 Class<U>,U
调用,但由于 Class<capture of ?>,capture of ?
方法的通配符返回类型,它当前正在用 invoke
调用。
我可以将 doSomething
更改为 doSomething(Class<?> aClass,Object anInstance)
,但随后我失去了类型安全性,这不一定是唯一调用该方法的地方。
我的问题是:在给定显式转换的情况下,为什么编译器不能推断它们具有相同的基础类型 U
?
编辑(2021 年 3 月 9 日):
我通过反编译字节码的自由来了解为什么 rzwitserloot's helper method 确实解决了类型问题。由于类型擦除,它们似乎是相同的调用。我猜编译器不够聪明,无法在转换后推断它们是相同的捕获类型,需要类型绑定来帮助。
private static <U> void doSomethingWithTypeBinding(Class<U> aClass,Object anObject) {
doSomething(aClass,aClass.cast(anObject));
}
private static void doSomethingUnsafe(Class<?> aClass,Object anInstance) {}
我现在分别从第 15 行和第 16 行调用
doSomethingWithTypeBinding(myMethod.getReturnType(),myInstance);
doSomethingUnsafe(myMethod.getReturnType(),myMethod.getReturnType().cast(myInstance));
产生以下字节码:
L5
LINENUMBER 15 L5
ALOAD 2
INVOKEVIRTUAL java/lang/reflect/Method.getReturnType ()Ljava/lang/Class;
ALOAD 3
INVOKESTATIC depinjection/handspun/services/MinimalExample.doSomethingWithTypeBinding (Ljava/lang/Class;Ljava/lang/Object;)V
L6
LINENUMBER 16 L6
ALOAD 2
INVOKEVIRTUAL java/lang/reflect/Method.getReturnType ()Ljava/lang/Class;
ALOAD 2
INVOKEVIRTUAL java/lang/reflect/Method.getReturnType ()Ljava/lang/Class;
ALOAD 3
INVOKEVIRTUAL java/lang/Class.cast (Ljava/lang/Object;)Ljava/lang/Object;
INVOKESTATIC depinjection/handspun/services/MinimalExample.doSomethingUnsafe (Ljava/lang/Class;Ljava/lang/Object;)V
// access flags 0xA
// signature <U:Ljava/lang/Object;>(Ljava/lang/Class<TU;>;TU;)V
// declaration: void doSomething<U>(java.lang.class<U>,U)
private static doSomething(Ljava/lang/Class;Ljava/lang/Object;)V
L0
LINENUMBER 30 L0
RETURN
L1
LOCALVARIABLE aClass Ljava/lang/Class; L0 L1 0
// signature Ljava/lang/Class<TU;>;
// declaration: aClass extends java.lang.class<U>
LOCALVARIABLE anInstance Ljava/lang/Object; L0 L1 1
// signature TU;
// declaration: anInstance extends U
MAXSTACK = 0
MAXLOCALS = 2
// access flags 0xA
// signature <U:Ljava/lang/Object;>(Ljava/lang/Class<TU;>;Ljava/lang/Object;)V
// declaration: void doSomethingWithTypeBinding<U>(java.lang.class<U>,java.lang.Object)
private static doSomethingWithTypeBinding(Ljava/lang/Class;Ljava/lang/Object;)V
L0
LINENUMBER 33 L0
ALOAD 0
ALOAD 0
ALOAD 1
INVOKEVIRTUAL java/lang/Class.cast (Ljava/lang/Object;)Ljava/lang/Object;
INVOKESTATIC depinjection/handspun/services/MinimalExample.doSomething (Ljava/lang/Class;Ljava/lang/Object;)V
L1
LINENUMBER 34 L1
RETURN
L2
LOCALVARIABLE aClass Ljava/lang/Class; L0 L2 0
// signature Ljava/lang/Class<TU;>;
// declaration: aClass extends java.lang.class<U>
LOCALVARIABLE anObject Ljava/lang/Object; L0 L2 1
MAXSTACK = 3
MAXLOCALS = 2
// access flags 0xA
// signature (Ljava/lang/Class<*>;Ljava/lang/Object;)V
// declaration: void doSomethingUnsafe(java.lang.class<?>,java.lang.Object)
private static doSomethingUnsafe(Ljava/lang/Class;Ljava/lang/Object;)V
L0
LINENUMBER 37 L0
RETURN
L1
LOCALVARIABLE aClass Ljava/lang/Class; L0 L1 0
// signature Ljava/lang/Class<*>;
// declaration: aClass extends java.lang.class<?>
LOCALVARIABLE anInstance Ljava/lang/Object; L0 L1 1
MAXSTACK = 0
MAXLOCALS = 2
由于它们的运行时类型擦除,我们可以看到 INVOKEVIRTUAL
强制转换到 INVOKESTATIC
中看起来完全相同。
编辑(2021 年 3 月 12 日):
@Holger pointed out in the comments,Method#getReturnType
返回 Class<?>
。因为它是通配符,所以从编译器的角度来看,该方法不能保证后续方法调用返回具有相同捕获类型的类。
解决方法
类型变量是编译器想象的虚构:它们在编译(擦除*)后无法生存。最好将它们视为链接事物。一个只在一个地方使用的类型变量是完全没有用的;一旦它们出现在两个地方,现在这很有用:它允许您将类型的多个用法链接在一起,也就是说出现是相同的。例如,您可以将 .add(Obj thingToAdd)
的参数类型和 .get(int idx)
的返回类型联系在一起以得到 java.util.List
。
在这里,您希望将 Class<X>
的 myMethod.getReturnType
与 myInstance
变量链接在一起。正如您所意识到的,这是不可能的,因为编译器不知道它们最终会是相同的类型。但是,通过调用 cast()
的 Class<X>
方法,我们可以解决该部分问题。
但是您仍然需要某种类型变量来充当将事物联系在一起的工具,而您没有。 ?
类似于 one-use-and-done 类型变量; Class<?> cls
和 myMethod.getReturnType().cast(myInstance)` 是“不同的”?你需要一个类型变量。你当然可以介绍一个:
private static <X> helper(Class<X> x,Object myInstance) {
doSomething(x,x.cast(myInstance));
}
将此方法添加到您的代码中并调用此方法,而不是调用 doSomething
。我们在此处创建的 <X>
用于将结果联系在一起。
*) 当然,它们仍然保留在公共签名中,但在其他任何地方,在运行时 - 它们都会被删除。
此处提供了另一种选择:doSomething
方法是私有,因此您可以完全控制它。因此,您可以直接将演员表移到其中来解决所有问题,或者,您可以这样编写:
/** precondition: o must be an instance of c */
private static void doSomething(Class<?> c,Object o) {
}
由于它是私有方法,因此可以引入先决条件。您可以完全控制调用此方法的所有代码。如果你真的想要,你可以添加一个运行时检查(顶部的 if (!c.isInstanceof(o)) throw new IllegalArgumentException("o not instance of c");
),但是否值得这样做是 Java 生态系统中关于私有方法的公开辩论。通常的结论是不这样做,或者为此使用 assert
关键字。
注意:这有一些糟糕的空/可选处理。万一找不到要解决的方法你..就默默的什么都不做?这就是 NPE 更好的原因:至少那样粗心的编码会导致异常而不是疯狂的追逐。
,首先调用的是:
Object myInstance = myMethod.invoke(null);
myMethod
是 static
(正如您在 resolveMethod
中已经找到的那样),因此您需要传递一个 null
,否则您将需要一个实例,您没有。
然后修复你的例子,是相当微不足道的:
Method myMethod = myOptMethod.get();
Object myInstance = myMethod.invoke(null);
Class<?> cls = myMethod.getReturnType();
Object obj = myMethod.getReturnType().cast(myInstance);
doSomething(cls,obj);
该方法将定义更改为:
private static <U> void doSomething(Class<? extends U> aClass,U anInstance) {....}