6

I just discovered this today when one of my unit tests failed because of upgrading from Java 7 to Java 8. The unit test calls a method which tries to find an annotation on a method which is annotated on a child class but with a different return type.

In Java 7, isAnnotationPresent seems to only find annotations if they were really declared in code. In Java 8, isAnnotationPresent seems to include annotations that were declared in child classes.

To illustrate this I created a simple (??) test class IAPTest (for IsAnnotationPresentTest).

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;

public class IAPTest {
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface Anno {
    }
    public static interface I {
    }
    public static interface IE extends I {
    }
    public static class A {
        protected I method() {
            return null;
        }
    }
    public static class B extends A {
        @Anno
        protected IE method() {
            return null;
        }
    }
    public static void main(String[] args) {
        for (Method method : B.class.getDeclaredMethods()) {
            if (method.getName().equals("method") && I.class.equals(method.getReturnType())) {
                System.out.println(method.isAnnotationPresent(Anno.class));
            }
        }
    }
}

On the latest Java 7 (1.7.0_79 at time of writing), this method prints "false". On the latest Java 8 (1.8.0_66 at time of writing), this method prints "true". I would intuitively expect it to print "false".

Why is this? Does this indicate a bug in Java or an intended change in how Java works?

EDIT: Just to show the exact commands I used to replicate this (in a directory with IAPTest.java identical to the code block above):

C:\test-isannotationpresent>del *.class

C:\test-isannotationpresent>set JAVA_HOME=C:\nma\Toolsets\AJB1\OracleJDK\jdk1.8.0_66

C:\test-isannotationpresent>set PATH=%PATH%;C:\nma\Toolsets\AJB1\OracleJDK\jdk1.8.0_66\bin

C:\test-isannotationpresent>java -version
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)

C:\test-isannotationpresent>javac IAPTest.java

C:\test-isannotationpresent>java IAPTest
true

C:\test-isannotationpresent>
Adam Burley
  • 5,551
  • 4
  • 51
  • 72
  • Hmm, this prints `false` for me: Eclipse Mars 4.5.1, JDK 1.8.0_51. – Tunaki Nov 05 '15 at 16:33
  • Why do you think that annotations “were declared in child classes”? You have annotated the method in `B` and you are searching the *declared methods* of `B` and nothing else. There are no child classes involved. – Holger Nov 05 '15 at 17:24
  • @Holger when I search for the declared methods of `B` I find two methods. One method represents the method on the child class and one represents the method on the parent class. You can tell this from the return types. `B.class.getDeclaredMethods().length == 2` on both Java 7 and 8. Because I am checking the return type to ensure that the parent class method is the one being referred to (return type is `I` rather than `IE`), the method in `A` does not have the annotation, but only has the annotation in the child class `B`. – Adam Burley Nov 05 '15 at 17:43
  • @Tunaki I just tested with your JDK version (build 1.8.0_51-b16) and I got "true". I'm running Java directly (not through Eclipse) – Adam Burley Nov 05 '15 at 17:49
  • @Holger I did not say that `B` has child classes (subclasses). `A` has subclasses. The method returned is a synthetic method from the parent class. I also never said I don't believe you that the method I'm looking at was not declared within `B`. I agree with you. I don't know why you seem angry towards me but anyway my question has been answered now. – Adam Burley Nov 05 '15 at 17:55
  • @Holger I do agree with you. If you invoke `getDeclaredMethods()` on `B` you get the declared methods of `B` and nothing else. I don't see any need to continue this discussion. – Adam Burley Nov 05 '15 at 18:00
  • So why did you write the opposite multiple times? – Holger Nov 05 '15 at 18:01
  • @Holger I used terms like "represents" and "from" to describe bridge methods, but I never said that I got declared methods from another class. I'm sorry if I wasn't clear. I bear no ill will towards you. I notice you have now deleted your earlier comments. – Adam Burley Nov 05 '15 at 18:14
  • In your question you are saying “*In Java 8, `isAnnotationPresent` seems to include annotations that were declared in child classes*” which is a conclusion that contradicts the fact that the annotation is present in `B` and you are only examining methods of `B`. Finding the annotation present in a class when you examine exactly that class isn’t surprising. And since you said in one comment “*Because I am checking the return type to ensure that the parent class method is the one being referred to*”, you *did* think you were getting a parent class’ method in the result of `getDeclaredMethods` – Holger Nov 06 '15 at 10:13
  • @Holger Again, I apologise if my wording did not clearly express to you what I was trying to say. Beyond that, I don't think there is a need to continue this discussion. Have a nice weekend. – Adam Burley Nov 06 '15 at 18:02

1 Answers1

10

I believe this is related to a change mentioned in the java 8 compatibility guide

As of this release, parameter and method annotations are copied to synthetic bridge methods.This fix implies that now for programs like:

@Target(value = {ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME) @interface ParamAnnotation {}  
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) @interface MethodAnnotation {}  
abstract class T<A,B> {
    B m(A a){
        return null;
    }  
}    
class CovariantReturnType extends T<Integer, Integer> {
    @MethodAnnotation
    Integer m(@ParamAnnotation Integer i) {
        return i;
    }

    public class VisibilityChange extends CovariantReturnType {}   
}  

Each generated bridge method will have all the annotations of the method it redirects to. Parameter annotations will also be copied. This change in the behavior may impact some annotations processor or in general any application that use the annotations.

The second method that returns an I instead of an IE is a synthetic method generated because you have a narrower return type in the overridden method than in the super class. Note that it's not present in the list of declared methods if you don't have a narrowing return type. So I think this is not a bug, but a deliberate change.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
whaleberg
  • 2,093
  • 22
  • 26
  • 1
    Great answer! Not only did you provide me with the reasoning, and documentation quote, but also the way to fix my unit test to work on Java 8 - I just need to use `isSynthetic`. Thanks! – Adam Burley Nov 05 '15 at 17:56