Компилятор Java не сохраняет общие аннотации методов?

В настоящее время я сталкиваюсь с проблемой с аннотациями стирания Java-типа общего вида и времени выполнения, и я не уверен, что я делаю что-то неправильно или это ошибка в компиляторе Java. Рассмотрим следующий минимальный рабочий пример:

@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) {
    }
}

Теперь, когда я запрашиваю информацию о MyClass.hello с отражением, я ожидаю, что метод hello все еще имеет аннотацию, однако это не так:

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));
        }
    }

}

Сообщение об ошибке (неожиданное) выглядит следующим образом:

java.lang.AssertionError: метод 'public void test.MyClass.hello(java.lang.Object) 'не аннотируется.

Протестировано с помощью Java 1.7.60.

Ответ 1

Как было указано другими, компиляция генерирует два метода с тем же именем: hello(Object) и a hello(MyObject).

Причиной этого является стирание типа:

MyGenericInterface mgi = new MyClass();
c.hello( "hahaha" );

Вышеприведенное должно компилироваться, поскольку стирание void hello(T) равно void hello(Object). Конечно, он также должен быть неудачным во время выполнения, потому что нет реализации, которая бы принимала произвольное Object.

Из вышесказанного можно сделать вывод, что void hello(MyObject) на самом деле не является допустимым переопределением для этого метода. Но дженерики были бы бесполезны, если вы не смогли бы "переопределить" метод с параметром типа.

Способ, которым компилятор обходит его, заключается в создании синтетического метода с сигнатурой void hello(Object), которая проверяет тип входного параметра во время выполнения и делегирует на void hello(MyObject), если проверка выполнена успешно. Как вы можете видеть в байтовом коде в ответ Джона Фаррелли.

Итак, ваш класс действительно выглядит примерно так (обратите внимание, как ваша аннотация остается на исходном методе):

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

     @Override
     public void hello(Object ob) {
         hello((MyObject)ob);
     }
}

К счастью, поскольку это синтетический метод, вы можете отфильтровать void hello(Object), проверив значение method.isSynthetic(), если оно истинно, вы должны просто игнорировать его для целей обработки аннотаций.

@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));
        }
    }
}

Это должно работать нормально.

Обновление: Согласно эта RFE, теперь аннотации должны быть скопированы и для мостовых методов.

Ответ 2

Кажется, что внутри javac создано 2 метода:

$ 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
}

Метод hello(java.lang.Object) проверяет тип объекта, а затем вызывает метод MyObject, который имеет аннотацию на нем.


Обновление

Я вижу, что эти дополнительные "мостовые методы" специально вызываются как часть стирания и дженериков типов:

https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html

Кроме того, аннотации, отсутствующие в этих мостах являются ошибкой, которая исправлена ​​в Java 8 u94

Ответ 3

Метод появляется дважды. Один из них аннотирован, а другой нет. Я думаю, это тоже ваше дело, но ошибка утверждения происходит на плохой, и вы не можете увидеть хорошее.

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));
    }
}

Выход:

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.

РЕДАКТИРОВАТЬ: Как я подтвердил, и другие подтвердили, что его метод дублируется компилятором. Это необходимо java. Я добрался до декомпилировать его правильный путь:

//# 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);
    }
}

Это связано с этим вопросом: Передача производного класса методу, который должен переопределить ожидающий базовый класс

Я думаю, что это тоже связано. Поле дублируется, потому что метод: Дублированное поле в сгенерированном XML с использованием JAXB