Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java's compiler not retaining generic method annotations?

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.

like image 949
Krassi Avatar asked Jun 26 '15 09:06

Krassi


People also ask

Why are generics erased by the Java compiler?

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.

What type of annotations will be retained by the compiler at compile time but will be ignored by the VM?

CLASS – The marked annotation is retained by the compiler at compile time, but is ignored by the Java Virtual Machine (JVM).

What is @override annotation in Java?

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.

Is @override a meta annotation?

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.


2 Answers

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.

like image 70
biziclop Avatar answered Nov 08 '22 08:11

biziclop


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

like image 3
John Farrelly Avatar answered Nov 08 '22 08:11

John Farrelly