Java 反射强制类型转换方法的返回类型

3
为了提供一些背景,我正在创建一个小的依赖注入器,并遇到将方法调用强制转换回其返回类型的问题。一个最简示例是:
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的助手方法确实解决了类型问题。由于类型擦除,它们似乎是相同的调用。我猜编译器只是不够聪明,在转换后无法推断它们是相同的捕获类型,需要类型绑定来帮助。

我已添加以下函数

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

我们可以看到 INVOKEVIRTUALINVOKESTATIC 在运行时类型擦除后看起来完全相同。


编辑(2021年3月12日):

@Holger在评论中指出Method#getReturnType返回一个Class<?>。因为它是通配符类型,所以从编译器的角度来看,该方法无法保证后续方法调用返回具有相同捕获类型的类。


1
附注:invokeMethodresolveMethod<T>类型参数是无意义的。你可以将方法的参数类型更改为Class<?>并删除类型参数,而不改变逻辑。 此外,您只需使用resolveMethod(aClass). ifPresent(myMethod-> helper(myMethod.getReturnType(), myMethod.invoke(null)),而不是 isPresent()get() 及其他中间步骤。 - Holger
2个回答

2

类型变量是编译器想象出来的东西:它们在编译(擦除)后不会保留。最好将它们视为链接物。一个仅在一个地方使用的类型变量完全没有用处;一旦它们出现在两个地方,那么就有用了:它可以将多个类型的用法链接在一起,以表明这些用法是相同的。例如,您可以将java.util.List的参数类型.add(Obj thingToAdd)和返回类型.get(int idx)绑定在一起。

在这里,您希望将myMethod.getReturnTypeClass<X>myInstance变量绑定在一起。正如您意识到的那样,这是不可能的,因为编译器不知道它们最终会成为相同的类型。但是,通过调用Class<X>cast()方法,我们解决了这个问题。

但是你仍然需要某些类型变量作为将事物联系在一起的工具,而你没有。 ?类似于一次性使用并完成的类型变量;Class<?> clsmyMethod.getReturnType().cast(myInstance)是“不同”的?s:是的,你的眼睛可以看出它们将成为相同的类型,但是java不能。你需要一个类型变量。当然,您可以引入一个:

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关键字进行操作。
注意:这里的null/可选项处理有些糟糕。如果找不到要解析的方法,则什么也不做吗?这就是为什么NPE更好的原因:至少粗心编码会导致异常而不是一场野鹅追逐。

我认为这篇文章很好地概述了为什么它们是“不同”的捕获;然而,我仍然不完全相信编译器为什么不能在myMethod.getReturnType().cast(myInstance)调用之后解决它们是相同的。毕竟,在绑定通配符到类型之后,辅助方法也会执行相同的操作。 - Andoni Michael
是的,正如所述,我拥有所有这些代码,因此我确实可以减少doSomething的类型安全性并确保它在上游或前提条件中,但我想了解为什么编译器无法推断它们是相同的类型。此外,这不是实际代码,而只是一个小的工作最小示例。真正的依赖注入器不会操作可选项或通过无参数工厂进行过滤,而是查找所有已注释的提供程序方法并具有强大的空处理 :) - Andoni Michael
3
对于编译器来说,getReturnType()只是一个方法。它并不知道该方法的契约保证了在两次调用时返回相同的Class对象。除此之外,它只是遵循规则。 - Holger
@Holger 天哪,我没想到这个。因为它返回了一个 Class<?>,所以不能保证返回相同的捕获。在这种情况下完全有意义!将更新描述。 - Andoni Michael

0
首先,调用是这样的:

Object myInstance = myMethod.invoke(null);

myMethodstatic 的(正如你在 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) {....}

2
这是误导性的;你总是会绑定对象并且没有类型安全,因为“Object”总是可以工作的。因此,这段代码是一个谎言(它表明存在安全性,但实际上不存在),最好写成:private static void doSomething(Class<?> c, Object instance) {}。现在很清楚,并且同样有效。 - rzwitserloot
@rzwitserloot 是的,这没有类型安全性,但我假设 OP 已经理解了。然而,有时候你确实想要捕获通配符,这就是为什么存在这样的方法的原因。 - Eugene
1
就像我说的,U总是对象,所以这并没有捕获到任何有用的东西。我的建议在这方面是一个严格的改进。 - rzwitserloot
是的,我在我的问题中注意到了相同的解决方案,但这会降低“doSomething”方法的类型安全性。这不是唯一的用例,我想确保“anInstance”确实是“aClass”的一种类型。问题更多地是关于为什么编译器不能推断它们是相同类型,因为强制转换起作用。 - Andoni Michael
还有感谢您提供的invoke(null)提示!代码可以正常工作,但是null可能是更好的值。根据JavaDocs:如果底层方法是静态的,则忽略指定的{@code obj}参数。它可以为null。 - Andoni Michael

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