【问题标题】:Java Reflection Casting Method ReturnTypeJava反射转换方法ReturnType
【发布时间】: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&lt;U&gt;, U 调用,但由于invoke 方法的通配符返回类型,它目前正在用Class&lt;capture of ?&gt;, capture of ? 调用。

我可以将doSomething 更改为doSomething(Class&lt;?&gt; 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&lt;?&gt;。因为它是通配符,所以从编译器的角度来看,该方法不能保证后续方法调用返回具有相同捕获类型的 Class。

【问题讨论】:

  • 旁注:invokeMethodresolveMethod&lt;T&gt; 类型参数没有意义。您可以将方法的参数类型更改为Class&lt;?&gt;,并在不更改逻辑的情况下删除类型参数。此外,您可以只使用resolveMethod(aClass).ifPresent(myMethod -&gt; helper(myMethod.getReturnType(), myMethod.invoke(null)); 而不是isPresent()get() 以及其他中间步骤。

标签: java generics reflection casting


【解决方案1】:

类型变量是编译器想象出来的:它们无法在编译后继续存在(擦除*)。最好将它们视为链接事物。永远只在一个地方使用的类型变量是完全没用的;一旦它们出现在两个地方,现在这很有用:它可以让您将类型的多种用法联系在一起,表示出现是相同的。例如,可以将.add(Obj thingToAdd)的参数类型和.get(int idx)的返回类型绑定在一起为java.util.List

在这里,您要将myMethod.getReturnTypeClass&lt;X&gt;myInstance 变量链接在一起。正如您所意识到的,这是不可能的,因为编译器知道它们最终会是同一类型。但是,通过调用Class&lt;X&gt;cast() 方法,我们可以解决这部分问题。

但是您仍然需要一些类型变量来充当将事物联系在一起的工具,而您没有。 ? 类似于一次性使用型变量; Class&lt;?&gt; cls 和 myMethod.getReturnType().cast(myInstance)` 是“不同的”?s:是的,你的眼球可以看出它将是相同的类型,但 java 不能。你需要一个类型变量。你当然可以介绍一个:

private static <X> helper(Class<X> x, Object myInstance) {
    doSomething(x, x.cast(myInstance));
}

将此方法添加到您的代码并调用此方法,而不是调用doSomething。我们在此处创建的&lt;X&gt; 用于将结果联系在一起。

*) 当然,它们保留在公共签名中,但在其他任何地方,在运行时 - 它们都会被删除。

您可以在此处使用另一种选择: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 更好的原因:至少这样粗心的编码会导致异常,而不是徒劳无功。

【讨论】:

  • 我认为这很好地概述了为什么它们是“不同的”捕获;但是,我仍然不是 100% 相信为什么编译器在 myMethod.getReturnType().cast(myInstance) 调用之后无法解析它们是相同的。毕竟,在将通配符绑定到类型之后,辅助方法会执行相同的操作
  • 是的,如上所述,我拥有所有这些代码,所以我确实可以降低 doSomething 的类型安全性并确保它在上游或在先决条件下,但我想了解为什么编译器不能推断它们是相同的类型。这也不是实际的代码,而只是一个小的工作示例。真正的依赖注入器不会对选项进行操作或通过无参数工厂进行过滤,但会找到所有带注释的提供程序方法并具有强大的 null 处理:)
  • @AndoniMichael 对于编译器来说,getReturnType() 只是一个方法。它不知道它的合约保证在两次调用中都返回相同的Class 对象。除此之外,它只是遵循规则。
  • @Holger 哇,我没想到。因为它返回一个Class&lt;?&gt;,所以不能保证返回相同的捕获。在那种光线下很有意义!将更新描述。
【解决方案2】:

首先调用的是:

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) {....}

【讨论】:

  • 这是误导; U 将始终绑定对象并且绝对不提供类型安全性,因为“对象”总是有效。因此,这段代码是一个谎言(它暗示了不存在的安全性),最好写成:private static void doSomething(Class&lt;?&gt; c, Object instance) {}。现在很清楚了,而且效果也很好。
  • @rzwitserloot 是的,这没有类型安全性,但我认为 OP 已经理解这一点。不过,有时您确实想捕获通配符,这正是存在此类方法的原因。
  • 正如我所说,U 将始终是对象,因此这不会捕获任何有用的信息。我的建议是严格意义上的改进。
  • 是的,我在我的问题中注意到了相同的解决方案,但这会降低 doSomething 方法的类型安全性。这不是唯一的用例,我想确保anInstance 确实是aClass 的一种。问题更多是关于为什么编译器不能推断出它们是相同的类型给定的演员作品。
  • 也感谢invoke(null) 的提示!代码按原样工作,但 null 可能是一个更好的值。来自 JavaDocs:If the underlying method is static, then the specified {@code obj} argument is ignored. It may be null.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-10
  • 2023-03-07
  • 1970-01-01
  • 1970-01-01
  • 2011-11-01
  • 1970-01-01
  • 2022-12-31
相关资源
最近更新 更多