Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
306 views
in Technique[技术] by (71.8m points)

java - Annotating the functional interface of a Lambda Expression

Java 8 introduces both Lambda Expressions and Type Annotations.

With type annotations, it is possible to define Java annotations like the following:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface MyTypeAnnotation {
    public String value();
}

One can then use this annotation on any type reference like e.g.:

Consumer<String> consumer = new @MyTypeAnnotation("Hello ") Consumer<String>() {
    @Override
    public void accept(String str) {
        System.out.println(str);
    }
};

Here is a complete example, that uses this annotation to print "Hello World":

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedType;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class Java8Example {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE_USE)
    public @interface MyTypeAnnotation {
        public String value();
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("World!", "Type Annotations!");
        testTypeAnnotation(list, new @MyTypeAnnotation("Hello ") Consumer<String>() {
            @Override
            public void accept(String str) {
                System.out.println(str);
            }
        });
    }

    public static void testTypeAnnotation(List<String> list, Consumer<String> consumer){
        MyTypeAnnotation annotation = null;
        for (AnnotatedType t : consumer.getClass().getAnnotatedInterfaces()) {
            annotation = t.getAnnotation(MyTypeAnnotation.class);
            if (annotation != null) {
                break;
            }
        }
        for (String str : list) {
            if (annotation != null) {
                System.out.print(annotation.value());
            }
            consumer.accept(str);
        }
    }
}

The output will be:

Hello World! 
Hello Type Annotations!

In Java 8 one can also replace the anonymous class in this example with a lambda expression:

public static void main(String[] args) {
    List<String> list = Arrays.asList("World!", "Type Annotations!");
    testTypeAnnotation(list, p -> System.out.println(p));
}

But since the compiler infers the Consumer type argument for the lambda expression, one is no longer able to annotate the created Consumer instance:

testTypeAnnotation(list, @MyTypeAnnotation("Hello ") (p -> System.out.println(p))); // Illegal!

One could cast the lambda expression into a Consumer and then annotate the type reference of the cast expression:

testTypeAnnotation(list,(@MyTypeAnnotation("Hello ") Consumer<String>) (p -> System.out.println(p))); // Legal!

But this will not produce the desired result, because the created Consumer class will not be annotated with the annotation of the cast expression. Output:

World!
Type Annotations!

Two questions:

  1. Is there any way to annotate a lambda expression similar to annotating a corresponding anonymous class, so one gets the expected "Hello World" output in the example above?

  2. In the example, where I did cast the lambda expression and annotated the casted type: Is there any way to receive this annotation instance at runtime, or is such an annotation always implicitly restricted to RetentionPolicy.SOURCE?

The examples have been tested with javac and the Eclipse compiler.

Update

I tried the suggestion from @assylias, to annotate the parameter instead, which produced an interesting result. Here is the updated test method:

public static void testTypeAnnotation(List<String> list, Consumer<String> consumer){
    MyTypeAnnotation annotation = null;
    for (AnnotatedType t :  consumer.getClass().getAnnotatedInterfaces()) {
        annotation = t.getAnnotation(MyTypeAnnotation.class);
        if (annotation != null) {
            break;
        }
    }
    if (annotation == null) {
            // search for annotated parameter instead
        loop: for (Method method : consumer.getClass().getMethods()) {
            for (AnnotatedType t : method.getAnnotatedParameterTypes()) {
                annotation = t.getAnnotation(MyTypeAnnotation.class);
                if (annotation != null) {
                    break loop;
                }
            }
        }
    }
    for (String str : list) {
        if (annotation != null) {
            System.out.print(annotation.value());
        }
        consumer.accept(str);
    }
}

Now, one can also produce the "Hello World" result, when annotating the parameter of an anonymous class:

public static void main(String[] args) {
    List<String> list = Arrays.asList("World!", "Type Annotations!");
    testTypeAnnotation(list, new Consumer<String>() {
        @Override
        public void accept(@MyTypeAnnotation("Hello ") String str) {
            System.out.println(str);
        }
    });
}

But annotating the parameter does not work for lambda expressions:

public static void main(String[] args) {
    List<String> list = Arrays.asList("World!", "Type Annotations!");
    testTypeAnnotation(list, (@MyTypeAnnotation("Hello ") String str) ->  System.out.println(str));
}

Interestingly, it is also not possible to receive the name of the parameter (when compiling with javac -parameter), when using a lambda expression. I'm not sure though, if this behavior is intended, if parameter annotations of lambdas have not yet been implemented, or if this should be considered a bug of the compiler.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

After digging into the Java SE 8 Final Specification I'm able to answer my questions.

(1) In response to my first question

Is there any way to annotate a lambda expression similar to annotating a corresponding anonymous class, so one gets the expected "Hello World" output in the example above?

No.

When annotating the Class Instance Creation Expression (§15.9) of an anonymous type, then the annotation will be stored in the class file either for the extending interface or the extending class of the anonymous type.

For the following anonymous interface annotation

Consumer<String> c = new @MyTypeAnnotation("Hello ") Consumer<String>() {
    @Override
    public void accept(String str) {
        System.out.println(str);
    }
};

the type annotation can then be accessed at runtime by calling Class#getAnnotatedInterfaces():

MyTypeAnnotation a = c.getClass().getAnnotatedInterfaces()[0].getAnnotation(MyTypeAnnotation.class);

If creating an anonymous class with an empty body like this:

class MyClass implements Consumer<String>{
    @Override
    public void accept(String str) {
        System.out.println(str);
    }
}
Consumer<String> c = new @MyTypeAnnotation("Hello ") MyClass(){/*empty body!*/};

the type annotation can also be accessed at runtime by calling Class#getAnnotatedSuperclass():

MyTypeAnnotation a = c.getClass().getAnnotatedSuperclass().getAnnotation(MyTypeAnnotation.class);

This kind of type annotation is not possible for lambda expressions.

On a side note, this kind of annotation is also not possible for normal class instance creation expressions like this:

Consumer<String> c = new @MyTypeAnnotation("Hello ") MyClass();

In this case, the type annotation will be stored in the method_info structure of the method, where the expression occurred and not as an annotation of the type itself (or any of its super types).

This difference is important, because annotations stored in the method_info will not be accessible at runtime by the Java reflection API. When looking at the generated byte code with ASM, the difference looks like this:

Type Annotation on an anonymous interface instance creation:

@Java8Example$MyTypeAnnotation(value="Hello ") : CLASS_EXTENDS 0, null
// access flags 0x0
INNERCLASS Java8Example$1

Type Annotation on a normal class instance creation:

NEW Java8Example$MyClass
@Java8Example$MyTypeAnnotation(value="Hello ") : NEW, null

While in the first case, the annotation is associated with the inner class, in the second case, the annotation is associated with the instance creation expression inside the methods byte code.

(2) In response to the comment from @assylias

You can also try (@MyTypeAnnotation("Hello ") String s) -> System.out.println(s) although I have not managed to access the annotation value...

Yes, this is actually possible according to the Java 8 specification. But it is not currently possible to receive the type annotations of the formal parameters of lambda expressions through the Java reflection API, which is most likely related to this JDK bug: Type Annotations Cleanup. Also the Eclipse Compiler does not yet store the relevant Runtime[In]VisibleTypeAnnotations attribute in the class file - the corresponding bug is found here: Lambda parameter names and annotations don't make it to class files.

(3) In response to my second question

In the example, where I did cast the lambda expression and annotated the casted type: Is there any way to receive this annotation instance at runtime, or is such an annotation always implicitly restricted to RetentionPolicy.SOURCE?

When annotating the type of a cast expression, this information also gets stored in the method_info structure of the class file. The same is true for other possible locations of type annotations inside the code of a method like e.g. if(c instanceof @MyTypeAnnotation Consumer). There is currently no public Java reflection API to access these code annotations. But since they are stored in the class file, it is at least potentially possible to access them at runtime - e.g. by reading the byte code of a class with an external library like ASM.

Actually, I managed to get my "Hello World" example working with a cast expression like

testTypeAnnotation(list,(@MyTypeAnnotation("Hello ") Consumer<String>) (p -> System.out.println(p)));

by parsing the calling methods byte code using ASM. But the code is very hacky and inefficient, and one should probably never do something like this in production code. Anyway, just for completeness, here is the complete working "Hello World" example:

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypePath;
import org.objectweb.asm.TypeReference;

public class Java8Example {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE_USE)
    public @interface MyTypeAnnotation {
        public String value();
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("World!", "Type Annotations!");
        testTypeAnnotation(list, new @MyTypeAnnotation("Hello ") Consumer<String>() {
            @Override
            public void accept(String str) {
                System.out.println(str);
            }
        });
        list = Arrays.asList("Type-Cast Annotations!");
        testTypeAnnotation(list,(@MyTypeAnnotation("Hello ") Consumer<String>) (p -> System.out.println(p)));
    }

    public static void testTypeAnnotation(List<String> list, Consumer<String> consumer){
        MyTypeAnnotation annotation = null;
        for (AnnotatedType t :  consumer.getClass().getAnnotatedInterfaces()) {
            annotation = t.getAnnotation(MyTypeAnnotation.class);
            if (annotation != null) {
                break;
            }
        }
        if (annotation == null) {
            // search for annotated parameter instead
            loop: for (Method method : consumer.getClass().getMethods()) {
                for (AnnotatedType t : method.getAnnotatedParameterTypes()) {
                    annotation = t.getAnnotation(MyTypeAnnotation.class);
                    if (annotation != null) {
                        break loop;
                    }
                }
            }
        }
        if (annotation == null) {
            annotation = findCastAnnotation();
        }
        for (String str : list) {
            if (annotation != null) {
                System.out.print(annotation.value());
            }
            consumer.accept(str);
        }
    }

    private static MyTypeAnnotation findCastAnnotation() {
        // foundException gets thrown, when the cast annotation is found or the search ends.
        // The found annotation will then be stored at foundAnnotation[0]
        final RuntimeException foundException = new RuntimeException();
        MyTypeAnnotation[] foundAnnotation = new MyTypeAnnotation[1];
        try {
            // (1) find the calling method
            StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
            StackTraceElement previous = null;
            for (int i = 0; i < stackTraceElements.length; i++) {
                if (stackTraceElements[i].getMethodName().equals("testTypeAnnotation")) {
                    previous = stackTraceElements[i+1];
                }
            }
            if (previous == null) {
                // shouldn't happen
                return null;
            }
            final String callingClassName = previous.getClassName();
            final String callingMethodName = previous.getMethodName();
            final int callingLineNumber = previous.getLineNumber();
            // (2) read and visit the calling class
            ClassReader cr = new ClassReader(callingClassName);
            cr.accept(new ClassVisitor(Opcodes.ASM5) {
                @Override
                public MethodVisitor visitMethod(int access, String name,String desc, String signature, String[] exceptions) {
                    if (name.equals(callingMethodName)) {
                        // (3) visit the calling method
                        return new MethodVisitor(Opcodes.ASM5) {
                            int lineNumber;
                            String type;
                            public void visitLineNumber(int line, Label start) {
                                this.lineNumber = line;
                            };
                            public void visitTypeInsn(int opcode, String type) {
                                if (opcode == Opcodes.CHECKCAST) {
                                    this.type = type;
                                } else{
                                    this.type = null;
                                }
                            };
                            public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
                                if (lineNumber == callingLineNumber) {
                                    // (4) visit the annotation, if this is the calling line number AND the annotation is 
                                    // of type MyTypeAnnotation AND it was a cast expression to "java.util.function.Consumer"
                                    if (desc.endsWith("Java8Example$MyTypeAnnotation;") && this.type != null && this.type.equals("java/util/function/Consumer")) {
                                        TypeReference reference = new TypeReference(typeRef);
                                        if (reference.getSort() == TypeReference.CAST) {
                                            return new AnnotationVisitor(Opcodes.ASM5) {
                                                public void visit(String name, final Object value) {
                                                    if (name.equals("value")) {
                                                        // Heureka! - we found the Cast Annotation
                                                        foundAnnotation[0] = new MyTypeAnnotation() {
                                                            @Override
                     

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...