【问题标题】:Java's compiler not retaining generic method annotations?Java的编译器不保留通用方法注释?
【发布时间】:2015-06-26 09:20:45
【问题描述】:

我目前遇到了与 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 测试。

【问题讨论】:

  • 您认为哪个更有可能是代码中的错误或 Java 编译器中的错误?
  • 如果你从接口/类中删除类型参数,你会得到什么?
  • 我无法使用 Java 7u80 和 Java 8u45 重现此问题。你确定你编译的类文件是最新的吗?
  • 您的样本工作正常。见here
  • 看起来它也被认为是一个错误:bugs.java.com/view_bug.do?bug_id=6695379

标签: java generics reflection annotations


【解决方案1】:

正如其他人所指出的,编译会生成两个同名的方法,一个hello(Object) 和一个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)。正如您在John Farrelly's answer 中的字节码中看到的那样。

所以你的类真的看起来像这样(观察你的注释是如何保留在原始方法上的):

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

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

幸运的是,因为它是一种合成方法,您可以通过检查method.isSynthetic() 的值来过滤掉void hello(Object),如果是真的,您应该忽略它以进行注释处理。

@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 RFE,注释现在也应该被复制到桥接方法中。

【讨论】:

  • 感谢您的扩展回答。我知道类型擦除和合成方法,所以我们最终或多或少地按照您建议的方式解决了这个问题 - 通过忽略编译器生成的方法。在我们的例子中,注解很重要,因为存在依赖于它的存在的 AOP 拦截器,但是只要 actual 方法被注解,我们就不太关心缺少元数据的合成方法..
  • @s7orm 忽略合成方法通常是一种有用的策略,因为 Java 可能会生成其他一些合成方法,例如访问内部类的私有字段。你真的不需要对这些方法做任何事情,最好的方法是编写一个实用方法来包装class.getDeclaredMethods() 并将它们全部过滤掉。那你就不用担心了。
  • 如果这样的方法是Class API 的一部分,那就太好了,但你不能拥有一切。 :)
  • !method.isSynthetic() 调用很有趣 - 我不知道那个!
【解决方案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

此外,这些桥接方法is a bug which is fixed in 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需要它。我以正确的方式到达decompile

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

和这个问题有关:Passing Derived Class to a method which needs to override expecting a base class

我认为这也是相关的。该字段是重复的,因为方法是: Duplicated field in generated XML using JAXB

【讨论】:

  • 我不希望这个答案被接受。我只是尽力提供我能提供的信息,而 cmets 对此有点短。
  • 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. 然而,这并不完全涵盖这种情况。但它是 API 文档中唯一提到返回两次的方法。
  • 是的,用 jdk1.7.0_80 编译这两种方法也都列出了,但是用 u60 编译只列出了原来的方法,所以显然它在某个时候被修复了。
  • @s7orm 我正在使用 jdk 1.8.0u45,它的最新分支,我复制了它。
猜你喜欢
  • 2012-09-27
  • 1970-01-01
  • 1970-01-01
  • 2012-10-22
  • 1970-01-01
  • 1970-01-01
  • 2013-03-16
  • 2015-07-27
  • 1970-01-01
相关资源
最近更新 更多