11

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.

Konstantin Yovkov
  • 62,134
  • 8
  • 100
  • 147
Krassi
  • 2,336
  • 3
  • 22
  • 30
  • 7
    Which do you think is more likely, an error in your code or a bug in the Java compiler? – Kayaman Jun 26 '15 at 09:30
  • If you remove the type argument from the interface/class, what do you get? – biziclop Jun 26 '15 at 09:32
  • I can't reproduce this problem with Java 7u80 and Java 8u45. Are you sure your compiled class files are up to date? – Jesper Jun 26 '15 at 09:35
  • Your sample works just fine. See [here](http://ideone.com/UMgDzl). – manish Jun 26 '15 at 09:43
  • I didn't test it, but if it really doesn't work on 1.7.60... I find it funny to see that it actually was java compiler error, and the solution was to update java compiler – Deltharis Jun 26 '15 at 09:50
  • @manish Your code does not probe it. Print the methods that give you a null too. – aalku Jun 26 '15 at 09:53
  • I think the problem is not with the method for being generic but about changing the signature. If I enumerate MyClass methods I get two versions, one with one signature and one with the other one. One anottated and the other not. – aalku Jun 26 '15 at 09:56
  • For me it fails on jdk 1.8.0_45 that is the latest of it's branch. – aalku Jun 26 '15 at 10:08
  • Just out of curiosity, what happens if you place the annotation on the interface instead? – biziclop Jun 26 '15 at 10:32
  • Well, contrary to popular belief I really thing it is (it was) a bug in the java compiler... I took my example, but instead of checking the annotations with an unit test, I took manish's approach of just printing them. I compiled everything twice - with u60 and u80 (as @Jesper said that it works with u80) and executed it with u51 and got two different results. The jar that was compiled with u60 printed only the original method (and NOT the synthetic method), while the u80 jar printed both... – Krassi Jun 26 '15 at 11:48
  • @s7orm It isn't a bug, it's a feature. :) – biziclop Jun 26 '15 at 11:48
  • 1
    Looks like it's also considered a bug: http://bugs.java.com/view_bug.do?bug_id=6695379 – John Farrelly Jun 26 '15 at 13:14
  • @Kayaman I think the link from John Farrelly answers your question. – Krassi Jun 26 '15 at 15:04
  • @s7orm That's not a bug, that's an enhancement ;) – Kayaman Jun 29 '15 at 05:08

3 Answers3

4

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.

Community
  • 1
  • 1
biziclop
  • 48,926
  • 12
  • 77
  • 104
  • Thanks for the extended answer. I am aware of the type erasure and synthetic methods, so we ended up solving the issue more or less the way you suggested - by ignoring the compiler generated methods. In our case the annotation is important, as there are AOP interceptors that depend on its presence, but as long as the _actual_ method is annotated, we dont care much about the synthetic method missing the metadata.. – Krassi Jun 26 '15 at 12:10
  • @s7orm Ignoring synthetic methods is a useful strategy in general, as there are other things Java might generate synthetic methods for, access of private fields of inner classes for example. You really don't need to do anything with those methods, the best way is to write a utility method that wraps `class.getDeclaredMethods()` and filters all of them out. Then you don't have to worry about it. – biziclop Jun 26 '15 at 12:13
  • It would be nice if such a method was part of the `Class` API but you can't have everything. :) – biziclop Jun 26 '15 at 12:15
  • The !method.isSynthetic() call is interesting - I wasn't aware of that one! – John Farrelly Jun 26 '15 at 12:24
3

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

John Farrelly
  • 7,289
  • 9
  • 42
  • 52
2

The method appears twice. One is annotated and the other one not. I guess that is your case too but the assertion error happens on the bad one and you can't get to see the good.

Method[] declaredMethods = MyClass.class.getDeclaredMethods();
for (Method method : declaredMethods) {
    System.out.println(String.format("Method: %s", method));
    for (Annotation a: method.getAnnotations()) {
        System.out.println(String.format("  Annotation: %s  of class %s", a, a.annotationType()));
    }
    for (Annotation a: method.getDeclaredAnnotations()) {
        System.out.println(String.format("  DeclaredAnnotation: %s  of class %s", a, a.annotationType()));
    }
    if (method.getDeclaredAnnotation(MyAnnotation.class) == null) {
        System.out.println(String.format(
                "  Method '%s' is not annotated.", method));
    }
}

Output is:

Method: public void MyClass.hello(MyObject)
  Annotation: @MyAnnotation()  of class interface MyAnnotation
  DeclaredAnnotation: @MyAnnotation()  of class interface MyAnnotation
Method: public void MyClass.hello(java.lang.Object)
  Method 'public void MyClass.hello(java.lang.Object)' is not annotated.

EDIT: As I supossed and others confirmed, he method gets duplicated by the compiler. It is needed by java. I got to decompile it the right way:

//# java -jar ........\cfr_0_101.jar MyClass --hidebridgemethods false
/*
 * Decompiled with CFR 0_101.
 */
import MyAnnotation;
import MyGenericInterface;
import MyObject;

public class MyClass
implements MyGenericInterface<MyObject> {
    @MyAnnotation
    @Override
    public void hello(MyObject there) {
    }

    @Override
    public /* bridge */ /* synthetic */ void hello(Object object) {
        MyClass myClass;
        myClass.hello((MyObject)object);
    }
}

It is related to this question: Passing Derived Class to a method which needs to override expecting a base class

I think this is related too. The field is duplicated because the method is: Duplicated field in generated XML using JAXB

Community
  • 1
  • 1
aalku
  • 2,860
  • 2
  • 23
  • 44
  • I don't expect this answer to be accepted. I just try to give the info I can and comments are a bit short for that. – aalku Jun 26 '15 at 10:26
  • 1
    `If this Class object represents a type that has multiple declared methods with the same name and parameter types, but different return types, then the returned array has a Method object for each such method. ` This doesn't exactly cover this case however. But it's the only mention in the API doc of methods returned twice. – biziclop Jun 26 '15 at 10:30
  • Yeah, compiling with jdk1.7.0_80 both methods are listed as well, but compiling with u60 only the original one is listed, so apparently it was fixed at some point. – Krassi Jun 26 '15 at 12:13
  • @s7orm I'm using jdk 1.8.0u45, the latest of it's branch, and I reproduce it. – aalku Jun 26 '15 at 12:17