反射 - Method::getGenericReturnType 无通用 - 可见性

Reflection - Method::getGenericReturnType no generic - visbility

描述

我有一个奇怪的问题,其中 Method::getGenericReturnType() 无法检索通用类型信息。

这是最小化版本:

public class Test {
    public static void main(String[] args) {
        Method method = B.class.getMethods()[0]; // foo() method inherited from A
        System.out.println(method.getGenericReturnType());
    }

    static class A {
        public List<String> foo() { return null; }
    }

    public static class B extends A {}
}

输出是

java.util.List

没有任何通用类型信息。这对我来说似乎很奇怪。

但是,将 A 的可见性更改为 public 并且它正确地给出了

java.util.List<java.lang.String>

问题

我不知道这是错误还是实际预期的行为。如果是预料之中的,背后的原因是什么?

我正在使用来自 AdoptOpenJDK 的 OpenJDK 15:

// javac
javac 15.0.1

// java
openjdk version "15.0.1" 2020-10-20
OpenJDK Runtime Environment AdoptOpenJDK (build 15.0.1+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 15.0.1+9, mixed mode, sharing)

朋友也可以转载于:


调查结果

我做了很多实验,发现 AB 之间唯一的可见性组合触发了这个问题是 BpublicA 而不是 public。任何其他组合,它再次按预期工作。所以只有

行为怪异。

我尝试在不同的文件中移动代码,将其放入不同的包中,在这里和那里添加或删除 static,但没有任何改变。

方法的source code我也查了一下,就是

public Type getGenericReturnType() {
  if (getGenericSignature() != null) {
    return getGenericInfo().getReturnType();
  } else { return getReturnType();}
}

其中 getGenericSignature() 依赖于在 Method 实例构造期间设置的 String signature。在上述情况下,由于某种原因,它似乎是 null


更新 1

我刚刚检查了 类 的字节码并在 B.class:

中找到了这个
  public Test$B();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Test$A."<init>":()V
         4: return
      LineNumberTable:
        line 16: 0

  public java.util.List foo();
    descriptor: ()Ljava/util/List;
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #7                  // Method Test$A.foo:()Ljava/util/List;
         4: areturn
      LineNumberTable:
        line 16: 0

对我来说,这看起来像 B,出于某种原因,创建了另一个方法 foo(),它只是将方法调用转发给 As foo(),因此没有通用类型信息。

而且在调用B.getDeclaredMethods()的时候其实是returns一个方法,即

public java.util.List Test$B.foo()

即使此方法应该排除继承方法(来自 documentation):

Returns an array containing Method objects reflecting all the declared methods of the class or interface represented by this Class object, including public, protected, default (package) access, and private methods, but excluding inherited methods.

如果 B 真的创建了一个包装器方法,那么现在就有意义了。

但是,为什么要创建这样一个方法呢?是否有解释此行为的 JLS 部分?

当您声明 A public 时,B.class.getMethods()[0] 未引用 B;它引用 A.foo(),因为 signature.

的存在,在其中声明方法并获取类型


声明 A 非 public 强制 B.class.getMethods()[0] 引用 B.foo()

由于没有继承方法的声明,无法通过对 getGenericReturnType 的调用获取类型,因为类型擦除应用于泛型。

在编译时:

List<String> foo() 变为 List foo().

这就是 B 可以提供给您的关于方法的 return 类型的所有信息,因为 B.foo() 中的签名刚刚被删除。


AB 都持有相同的 return 类型 foo() : java.util.List无参数类型

这是 A.foo() 声明的 return 类型。与 B.foo():

相同

区别在于A有一个有效的签名,所以它会通过添加参数类型来完成调用getGenericReturnType()的结果。

B 的情况下,它只会显示它知道的内容:只是 return 类型。


我在尝试解决可见性难题时感到非常头疼。有关为什么会发生这种情况的完整解释,请查看 Eugene 的回答。说真的,你可以从这种人身上学到很多。

这里慢慢来吧。首先,here is why 生成桥接方法。即使你放弃泛型,仍然会有一个桥接方法。即这段代码:

static class A {
    public String foo() { return null; }
}

public static class B extends A {}

仍会生成一个带有 ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETICfoo 方法。您可以阅读错误描述并理解为什么需要这样做。

另一方面,如果你使Apublic,这样的方法将不会被生成,原因应该很明显,考虑到前面的错误解释(我希望)。所以这个想法是,如果你有一个 non-public class,javac 将为上面的场景生成一个桥接方法。


现在,如果您将泛型添加到合成方法的组合中,事情就会开始变得清晰起来。例如,你有这个:

interface WithGeneric<T> {
    public WithGeneric<T> self(T s);
}

public class Impl implements WithGeneric<String> {

    @Override
    public WithGeneric<String> self(String s) {
        return null;
    }
}

Impl.class也会生成桥接方法,但是它的声明会是接口的擦除。也就是说两个方法在Impl.class:

public WithGeneric<String> self(String) {...}

public WithGeneric self(Object) {...}

如果你把这两件事粘在一起:

  • 在非public classes 的情况下,将创建一个桥接方法(以便反射起作用)

  • 如果是泛型,将创建一个 erased 桥接方法(这样 erased 调用就可以工作)

事情会(以某种方式)有意义。