Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Annotations containing lambda expressions in Java 8

I am experimenting with static fields in annotations and I am stumbling upon something I do not understand.

I use the following code:

@a
public class myAnnotMinimal {
    public static void main(String[] args) {
        System.out.println(myAnnotMinimal.class.getAnnotations().length);
    }
}

@Retention(RetentionPolicy.RUNTIME)
@interface a {
    Consumer<Integer> f1 = a -> {return;}; // -> 0
    //Consumer<Integer> f2 = new B();  //-> 1
    //Consumer<Integer> f3 = C::eat; //-> 1
    //int f4 = 5; //->1
    //Supplier<Integer> f5 = ()->5; //->1
}

class B implements Consumer<Integer> {
    @Override
    public void accept(Integer t) {
    }
}

class C{
    public static void eat(Integer t){
    }
}

Now, when running this I would expect '1' to be printed, however, the output I get is '0'. When I remove the f1 field and uncomment the other (f2-f5) fields the output is '1'. To me this pretty much looks like a bug. Is there something I am missing? I use jdk1.8.0_66 on linux.

Since this looks like a bug in the JDK I filed a bug report. By now the bug report has been accepted. See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8147585 .

like image 715
miselico Avatar asked Jan 12 '16 17:01

miselico


1 Answers

This is a bug in the JRE’s annotation processing or, more precisely, its code hasn’t updated properly to accommodate the new language features, though it’s understandable that such usage hasn’t been considered.

Annotations are not supposed to provide utility methods and the ability to declare constants, i.e. fields that are implicitly public static final, should not be abused to add functions which carry code.

The reason for the bug is that certain Java language constructs produce synthetic methods behind the scenes. Field initializers may add code to a static method <clinit> at bytecode level, which is known and ignored by Reflection, thus creates no problems. That’s why Consumer<Integer> f2 = new B(); works (as it would do before Java 8), the creation happens inside this class initialization method. Note that int f4 = 5; creates a compile-time constant which does not need initialization code at all.

However, lambda expressions are compiled into synthetic methods which apparently are not ignored by the runtime Annotation implementation, despite being private, but checked for conformance with the standard annotation methods.

That’s why Supplier<Integer> f5 = ()->5; creates no problems, the synthetic method incidentally conforms to the annotation method pattern, as it has no parameters and returns a value. In contrast, Consumer<Integer> f1 = i->System.out.println(i); is compiled into a synthetic method having one parameter and void return type. This seems to cause the Annotation processing facility to reject that annotation as invalid (without reporting it).

In contrast, most method references do not need a synthetic helper method as they direct to the target method, thus Consumer<Integer> f3 = C::eat; creates no problems. You can verify this pattern by changing the declaration of f1 to Consumer<Integer> f1 = System.out::println;, while being semantically equivalent, the problem disappears, as now there’s no offending synthetic method in the annotation class file.

While this is indeed a bug, as, when when the Java language accepts lambda expression inside annotation fields, the JRE implementation should catch up to handle that scenario, I strongly discourage you from adding code to annotation types that way. Saving a single utility class is not worth that code smell. Also keep in mind that this creates additional runtime overhead compared to ordinary utility methods.

like image 54
Holger Avatar answered Oct 04 '22 13:10

Holger