Description
I have an odd issue in which Method::getGenericReturnType() is not able to retrieve the generic type information.
Here is the minimized version:
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 {}
}
Output is
java.util.List
without any generic type information. This seems odd to me.
However, changing A
s visibility to public
and it correctly gives
java.util.List<java.lang.String>
Question
I do not know if this is a bug or actually expected behavior. If it is expected, what is the reasoning behind it?
I am using OpenJDK 15 from AdoptOpenJDK:
// 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)
A friend could also reproduce it in:
- JDK Zulu 8
- AdoptOpenJDK 10, 11, 14
Findings
I experimented a lot and figured out that the only visibility-combination between A
and B
that triggers this issue is when B
is public
and A
is not public
. Any other combination and it works as expected again. So only
- B
public
, Aprotected
- B
public
, Apackage-visible
- B
public
, Aprivate
show weird behavior.
I tried moving the code around in different files, putting it into different packages, adding or removing static
here and there and nothing changed.
I also checked the source code of the method, which is
public Type getGenericReturnType() {
if (getGenericSignature() != null) {
return getGenericInfo().getReturnType();
} else { return getReturnType();}
}
where getGenericSignature()
relies on a String signature
that is set during construction of the Method
instance. It appears that it is null
for some reason in above situation.
Update 1
I was just checking out the bytecode of the classes and found this in 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
To me this looks like B
, for some reason, created another method foo()
that simply forwards the method call to A
s foo()
and hence has no generic type information.
Moreover, when calling B.getDeclaredMethods()
it actually returns a method, namely
public java.util.List Test$B.foo()
Even though this method is supposed to exclude inherited methods (from the 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.
Which would now make sense if B
really created a wrapper-method.
However, why is it creating such a method? Is there maybe a JLS section that explains this behavior?
B.class.getDeclaredMethods()
?getMethods()
only picks uppublic
members, maybe that has some impact. (Only an assumption) – Florriefoo
.toGenericString()
on it givespublic java.util.List Test$B.foo()
. I am surprised by this, as this method is supposed to exclude inherited methods (from doc: but excluding inherited methods.). – MucinB
appears to list an additionalfoo()
method (without generics) that just wrapsA
s truefoo()
method (updated the question). – MucinB
during type erasure ofA#foo
and that reflection is picking that up. That would explain the observations but I do not really know why visibility plays a role in that process. See docs.oracle.com/javase/tutorial/java/generics/… – Mucinfoo()
... but I think you're on the right track – Florrie