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>
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:
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
public
, A protected
public
, A package-visible
public
, A private
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.
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?
You can get around the superfluous reference by providing a generic static factory method. Something like public static <T> GenericClass<T> of(Class<T> type) {...} and then call it as such: GenericClass<String> var = GenericClass. of(String. class) .
The short answer is, that there is no way to find out the runtime type of generic type parameters in Java. A solution to this is to pass the Class of the type parameter into the constructor of the generic type, e.g.
(Yes, this is legal code; see Java Generics: Generic type defined as return type only.) The return type will be inferred from the caller.
Let's take it slow here. First of all, here is why a bridge method is generated to begin with. Even if you drop generics, there will still be a bridge method. That is, this code:
static class A {
public String foo() { return null; }
}
public static class B extends A {}
will still generate a foo
method with ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
. You can read the bug description and understand why that is needed.
On the other hand if you make A
public
, such a method will not be generated, the reason should be obvious, considering the previous bug explanation (I hope). So the idea is that if you have a non-public class, javac
will generate a bridge method for the scenario above.
Now, if you add generics into that mix of synthetic methods, things start to shed some light. For example, you have this:
interface WithGeneric<T> {
public WithGeneric<T> self(T s);
}
public class Impl implements WithGeneric<String> {
@Override
public WithGeneric<String> self(String s) {
return null;
}
}
There will be a bridge method generated too in Impl.class
, but its declaration is going to be the erasure of the interface. In other words there will be two methods in Impl.class
:
public WithGeneric<String> self(String) {...}
public WithGeneric self(Object) {...}
If you glue these two things:
in case of non-public classes a bridge method will be created (so that reflection would work)
in case of generics an erased bridge method will be created (so that erased calls would work)
things will make (somehow) sense.
When you declare A
public, B.class.getMethods()[0]
is not referencing B
; It is referencing A.foo()
, where the method is declared and the type is obtained because of the existance of the signature
.
Declaring A
non-public forces B.class.getMethods()[0]
reference B.foo()
.
As there is no declaration of the inherited method, the type can't be obtained from the call to getGenericReturnType
due to type erasure being applied to generics.
In compile time:
List<String> foo()
becomes
List foo()
.
And that's all the information B
could give to you regarding the method's return type, since the signature in B.foo()
was just removed.
Both A
and B
hold the same return type for foo()
: java.util.List
(without parameter type)
This is A.foo()
's declared return type. The same than B.foo()
:
The difference is that A
has a valid signature, so it will complete the result from the call to getGenericReturnType()
by adding the parameter types.
In the case of B
, it will only show what it knows: just the return type.
I suffered strong headaches trying to solve the visibility puzzle. For a complete explanation of why this happens, look up at Eugene's answer. Seriously, you can learn a lot from this type of guy.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With