I am currently encountering an issue with Java's generic type erasure and runtime annotations and I am not sure whether I am doing something wrong or it is a bug in the Java compiler. Consider the following minimal working example:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MyAnnotation {
}
public interface MyGenericInterface<T> {
void hello(T there);
}
public class MyObject {
}
public class MyClass implements MyGenericInterface<MyObject> {
@Override
@MyAnnotation
public void hello(final MyObject there) {
}
}
Now when I query information about MyClass.hello with reflection I would expect that the hello method still has the annotation, however it does not:
public class MyTest {
@Test
public void testName() throws Exception {
Method[] declaredMethods = MyClass.class.getDeclaredMethods();
for (Method method : declaredMethods) {
Assert.assertNotNull(String.format("Method '%s' is not annotated.", method), method
.getAnnotation(MyAnnotation.class));
}
}
}
The (unexpected) error message reads as follows:
java.lang.AssertionError: Method 'public void test.MyClass.hello(java.lang.Object)' is not annotated.
Tested with Java 1.7.60.
Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure to: Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded.
CLASS – The marked annotation is retained by the compiler at compile time, but is ignored by the Java Virtual Machine (JVM).
The @Override annotation denotes that the child class method overrides the base class method. For two reasons, the @Override annotation is useful. If the annotated method does not actually override anything, the compiler issues a warning. It can help to make the source code more readable.
The @Override annotation is a type of marker annotation that is used above a method to indicate to the Java compiler that the subclass method is overriding the superclass method.
As has been pointed out by others, compilation generates two methods with the same name, a hello(Object)
and a hello(MyObject)
.
The reason for this is type erasure:
MyGenericInterface mgi = new MyClass();
c.hello( "hahaha" );
The above should compile because the erasure of void hello(T)
is void hello(Object)
. Of course it should also fail at runtime because there is no implementation that would accept an arbitrary Object
.
From the above we can conclude that void hello(MyObject)
is not in fact a valid override for that method. But generics would be really useless if you couldn't "override" a method with a type parameter.
The way the compiler gets around it is to generate a synthetic method with the signature void hello(Object)
, which checks the input parameter type at runtime and delegates to void hello(MyObject)
, if the check is successful. As you can see in the byte code in John Farrelly's answer.
So your class really looks something like this (observe how your annotation stays on the original method):
public class MyClass implements MyGenericInterface<MyObject> {
@MyAnnotation
public void hello(final MyObject there) {
}
@Override
public void hello(Object ob) {
hello((MyObject)ob);
}
}
Luckily, because it's a synthetic method, you can filter out void hello(Object)
by checking the value of method.isSynthetic()
, if it's true you should just ignore it for the purposes of annotation processing.
@Test
public void testName() throws Exception {
Method[] declaredMethods = MyClass.class.getDeclaredMethods();
for (Method method : declaredMethods) {
if (!method.isSynthetic()) {
Assert.assertNotNull(String.format("Method '%s' is not annotated.", method), method
.getAnnotation(MyAnnotation.class));
}
}
}
This should work fine.
Update: According to this RFE, annotations should now be copied across to bridge methods as well.
It seems that internally, javac
has created 2 methods:
$ javap -c MyClass.class
Compiled from "MyTest.java"
class MyClass implements MyGenericInterface<MyObject> {
MyClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object." <init>":()V
4: return
public void hello(MyObject);
Code:
0: return
public void hello(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #2 // class MyObject
5: invokevirtual #3 // Method hello:(LMyObject;)V
8: return
}
The hello(java.lang.Object)
method checks the type of the object, and then invokes the MyObject
method, which has the annotation on it.
Update
I see that these additional "bridge methods" are specifically called out as part of type erasure and generics:
https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html
Also, the annotations missing on these bridged methods is a bug which is fixed in Java 8 u94
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