Java 注释 Lambda 表达式的函数接口

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22375891/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 15:23:05  来源:igfitidea点击:

Annotating the functional interface of a Lambda Expression

javalambdaannotationsjava-8

提问by Balder

Java 8 introduces both Lambda Expressionsand Type Annotations.

Java 8 引入了Lambda 表达式类型注释

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

使用类型注释,可以像下面这样定义 Java 注释:

@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":

这是一个完整的示例,它使用此注释打印“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:

在 Java 8 中,还可以用 lambda 表达式替换此示例中的匿名类:

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:

但是由于编译器为 lambda 表达式推断了 Consumer 类型参数,因此无法再注释创建的 Consumer 实例:

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:

可以将 lambda 表达式转换为 Consumer,然后注释转换表达式的类型引用:

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:

但这不会产生想要的结果,因为创建的 Consumer 类不会使用 cast 表达式的注解进行注解。输出:

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?

  1. 有什么方法可以像注释相应的匿名类那样注释 lambda 表达式,从而在上面的示例中获得预期的“Hello World”输出?

  2. 在示例中,我确实转换了 lambda 表达式并注释了转换类型:有没有办法在运行时接收这个注释实例,或者这样的注释是否总是隐式限制为 RetentionPolicy.SOURCE?

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

这些示例已经使用 javac 和 Eclipse 编译器进行了测试。

Update

更新

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

我尝试了@assylias 的建议,改为注释参数,这产生了一个有趣的结果。这是更新的测试方法:

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:

现在,当注释匿名类的参数时,还可以产生“Hello World”结果:

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 notwork for lambda expressions:

但标注的参数并没有lambda表达式的工作:

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.

有趣的是,当使用 lambda 表达式时,也无法接收参数的名称(当使用 javac -parameter 编译时)。但我不确定,如果这种行为是有意的,是否尚未实现 lambda 的参数注释,或者这是否应该被视为编译器的错误。

采纳答案by Balder

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

在深入研究Java SE 8 Final Specification 后,我可以回答我的问题。

(1) In response to my first question

(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?

有什么方法可以像注释相应的匿名类那样注释 lambda 表达式,从而在上面的示例中获得预期的“Hello World”输出?

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.

注释Class Instance Creation Expression (§15.9)匿名类型时,注释将存储在类文件中,用于扩展接口或匿名类型的扩展类。

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 runtimeby calling Class#getAnnotatedInterfaces():

然后可以通过调用在运行时访问类型注释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 runtimeby calling Class#getAnnotatedSuperclass():

还可以通过调用在运行时访问类型注释Class#getAnnotatedSuperclass()

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

This kind of type annotation is notpossible for lambda expressions.

这种类型的注释是没有可能的lambda表达式。

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 structureof the method, where the expression occurred and not as an annotation of the type itself (or any of its super types).

在这种情况下,类型注释将存储在方法的method_info 结构中,表达式出现在该结构中,而不是作为类型本身(或其任何超类型)的注释。

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

这种差异很重要,因为Java 反射 API 在运行时无法访问存储在 method_info 中的注释。使用ASM查看生成的字节码时,差异如下所示:

Type Annotation on an anonymous interface instance creation:

匿名接口实例创建的类型注解:

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

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 creationexpression inside the methods byte code.

在第一种情况下,注解与内部类相关联,在第二种情况下,注解与方法字节码内的实例创建表达式相关联。

(2) In response to the comment from @assylias

(2) 回应@assylias 的评论

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

您也可以尝试 (@MyTypeAnnotation("Hello ") String s) -> System.out.println(s) 虽然我还没有设法访问注释值...

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.

是的,根据 Java 8 规范,这实际上是可能的。但是目前还不能通过Java反射API来接收lambda表达式的形参的类型注解,这很可能与这个JDK bug:Type Annotations Cleanup有关。此外,Eclipse 编译器尚未在类文件中存储相关的 Runtime[In]VisibleTypeAnnotations 属性 - 此处找到了相应的错误:Lambda 参数名称和注释不会将其添加到类文件中。

(3) In response to my second question

(3) 回答我的第二个问题

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?

在示例中,我确实转换了 lambda 表达式并注释了转换类型:有没有办法在运行时接收这个注释实例,或者这样的注释是否总是隐式限制为 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.

注释转换表达式的类型时,此信息也存储在类文件的 method_info 结构中。对于方法代码内的其他可能的类型注释位置也是如此if(c instanceof @MyTypeAnnotation Consumer)。目前没有公共 Java 反射 API 来访问这些代码注释。但是由于它们存储在类文件中,因此至少有可能在运行时访问它们——例如,通过使用像ASM这样的外部库读取类的字节码。

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

实际上,我设法让我的“Hello World”示例使用了像这样的转换表达式

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:

通过使用 ASM 解析调用方法字节码。但是代码非常笨拙且效率低下,人们可能永远不应该在生产代码中做这样的事情。无论如何,为了完整起见,这里是完整的“Hello World”示例:

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
                                                            public Class<? extends Annotation> annotationType() {
                                                                return MyTypeAnnotation.class;
                                                            }
                                                            @Override
                                                            public String value() {
                                                                return value.toString();
                                                            }
                                                        };
                                                        // stop search (Annotation found)
                                                        throw foundException;
                                                    }
                                                };
                                            };
                                        }
                                    }
                                } else if (lineNumber > callingLineNumber) {
                                    // stop search (Annotation not found)
                                    throw foundException;
                                }
                                return null;
                            };

                        };
                    }
                    return null;
                }
            }, 0);
        } catch (Exception e) {
            if (foundException == e) {
                return foundAnnotation[0];
            } else{
                e.printStackTrace();
            }
        }
        return null;
    }
}

回答by user2219808

One possible work around that might be of use is to define empty interfaces that extend the interface that the lambda is going to implement and then cast to this empty interface just to use the annotation. Like so:

一种可能有用的解决方法是定义空接口来扩展 lambda 将要实现的接口,然后强制转换为这个空接口只是为了使用注释。像这样:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.function.Consumer;

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

    @MyAnnotation("Get this")
    interface AnnotatedConsumer<T> extends Consumer<T>{};

    public static void main( String[] args )
    {
        printMyAnnotationValue( (AnnotatedConsumer<?>)value->{} );
    }

    public static void printMyAnnotationValue( Consumer<?> consumer )
    {
        Class<?> clas = consumer.getClass();
        MyAnnotation annotation = clas.getAnnotation( MyAnnotation.class );
        for( Class<?> infClass : clas.getInterfaces() ){
            annotation = infClass.getAnnotation( MyAnnotation.class );
            System.out.println( "MyAnnotation value: " + annotation.value() );
        }
    }
}

The annotation is then available on the interfaces implemented by the class and is reusable if you want the same annotation elsewhere.

然后该注解可在类实现的接口上使用,如果您想在其他地方使用相同的注解,则可以重用该注解。