【发布时间】:2021-03-09 17:47:04
【问题描述】:
为了提供一些背景知识,我正在创建一个小型依赖注入器,并在将方法调用转换回其返回类型时遇到了问题。一个最小的例子是:
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 调用,但由于invoke 方法的通配符返回类型,它目前正在用Class<capture of ?>, capture of ? 调用。
我可以将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<?>。因为它是通配符,所以从编译器的角度来看,该方法不能保证后续方法调用返回具有相同捕获类型的 Class。
【问题讨论】:
-
旁注:
invokeMethod和resolveMethod的<T>类型参数没有意义。您可以将方法的参数类型更改为Class<?>,并在不更改逻辑的情况下删除类型参数。此外,您可以只使用resolveMethod(aClass).ifPresent(myMethod -> helper(myMethod.getReturnType(), myMethod.invoke(null));而不是isPresent()和get()以及其他中间步骤。
标签: java generics reflection casting