Anotando la interfaz funcional de una expresión Lambda

Java 8 presenta expresiones Lambda y anotaciones de tipo .

Con las anotaciones de tipo, es posible definir anotaciones Java como las siguientes:

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

Uno puede usar esta anotación en cualquier referencia de tipo como, por ejemplo:

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

Aquí hay un ejemplo completo, que usa esta anotación para imprimir “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 list = Arrays.asList("World!", "Type Annotations!"); testTypeAnnotation(list, new @MyTypeAnnotation("Hello ") Consumer() { @Override public void accept(String str) { System.out.println(str); } }); } public static void testTypeAnnotation(List list, Consumer 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); } } } 

El resultado será:

 Hello World! Hello Type Annotations! 

En Java 8 también se puede reemplazar la clase anónima en este ejemplo con una expresión lambda:

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

Pero dado que el comstackdor infiere el argumento de tipo de consumidor para la expresión lambda, ya no es posible anotar la instancia de consumidor creada:

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

Uno podría convertir la expresión lambda en un consumidor y luego anotar la referencia de tipo de la expresión moldeada:

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

Pero esto no producirá el resultado deseado, porque la clase Consumidor creada no se anotará con la anotación de la expresión moldeada. Salida:

 World! Type Annotations! 

Dos preguntas:

  1. ¿Hay alguna manera de anotar una expresión lambda similar a anotar una clase anónima correspondiente, por lo que uno obtiene el resultado esperado “Hola mundo” en el ejemplo anterior?

  2. En el ejemplo, donde eché la expresión lambda y anoté el tipo encasillado: ¿hay alguna forma de recibir esta instancia de anotación en el tiempo de ejecución, o dicha anotación siempre está implícitamente restringida a RetentionPolicy.SOURCE?

Los ejemplos han sido probados con javac y el comstackdor de Eclipse.

Actualizar

Probé la sugerencia de @assylias, para anotar el parámetro en su lugar, lo que produjo un resultado interesante. Aquí está el método de prueba actualizado:

 public static void testTypeAnnotation(List list, Consumer 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); } } 

Ahora, también se puede producir el resultado “Hello World” al anotar el parámetro de una clase anónima:

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

Pero anotar el parámetro no funciona para las expresiones lambda:

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

Curiosamente, tampoco es posible recibir el nombre del parámetro (cuando se comstack con el parámetro javac), cuando se utiliza una expresión lambda. Aunque no estoy seguro, si se pretende este comportamiento, si las anotaciones de parámetros de lambdas aún no se han implementado, o si esto se debe considerar como un error del comstackdor.

Después de profundizar en la especificación final de Java SE 8, puedo responder mis preguntas.

(1) En respuesta a mi primera pregunta

¿Hay alguna manera de anotar una expresión lambda similar a anotar una clase anónima correspondiente, por lo que uno obtiene el resultado esperado “Hola mundo” en el ejemplo anterior?

No.

Al anotar la Class Instance Creation Expression (§15.9) de un tipo anónimo, la anotación se almacenará en el archivo de clase ya sea para la interfaz extendida o para la clase extendida del tipo anónimo.

Para la siguiente anotación de interfaz anónima

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

la anotación tipo se puede acceder en tiempo de ejecución llamando a la Class#getAnnotatedInterfaces() :

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

Si crea una clase anónima con un cuerpo vacío como este:

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

también se puede acceder a la anotación de tipo en tiempo de ejecución llamando a Class#getAnnotatedSuperclass() :

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

Este tipo de anotación de tipo no es posible para las expresiones lambda.

En una nota lateral, este tipo de anotación tampoco es posible para las expresiones de creación de instancias de clases normales como esta:

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

En este caso, la anotación de tipo se almacenará en la estructura method_info del método, donde se produjo la expresión y no como una anotación del tipo en sí (o cualquiera de sus súper tipos).

Esta diferencia es importante, ya que las anotaciones almacenadas en method_info no serán accesibles en tiempo de ejecución por la API de reflexión de Java. Al mirar el código de bytes generado con ASM , la diferencia se ve así:

Escriba Annotation en una creación de instancia de interfaz anónima:

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

Escriba Annotation en una creación de instancia de clase normal:

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

Mientras que en el primer caso, la anotación está asociada con la clase interna , en el segundo caso, la anotación está asociada con la expresión de creación de instancia dentro del código de bytes de métodos.

(2) En respuesta al comentario de @assylias

También puedes probar (@MyTypeAnnotation (“Hello”) String s) -> System.out.println (s) aunque no he logrado acceder al valor de la anotación …

Sí, esto es realmente posible de acuerdo con la especificación Java 8. Pero actualmente no es posible recibir las anotaciones tipo de los parámetros formales de las expresiones lambda a través de la API de reflexión de Java, que probablemente esté relacionada con este error JDK: Escriba Anotaciones limpieza . Además, el comstackdor Eclipse aún no almacena el atributo Runtime [In] VisibleTypeAnnotations relevante en el archivo de clase; el error correspondiente se encuentra aquí: los nombres de los parámetros Lambda y las anotaciones no llegan a los archivos de clase.

(3) En respuesta a mi segunda pregunta

En el ejemplo, donde eché la expresión lambda y anoté el tipo encasillado: ¿hay alguna forma de recibir esta instancia de anotación en el tiempo de ejecución, o dicha anotación siempre está implícitamente restringida a RetentionPolicy.SOURCE?

Al anotar el tipo de expresión moldeada, esta información también se almacena en la estructura method_info del archivo de clase. Lo mismo es cierto para otras ubicaciones posibles de anotaciones de tipo dentro del código de un método como, por ejemplo, if(c instanceof @MyTypeAnnotation Consumer) . Actualmente no hay una API pública de reflexión de Java para acceder a estas anotaciones de código. Pero dado que están almacenados en el archivo de clase, al menos es posible acceder a ellos en tiempo de ejecución, por ejemplo, leyendo el código de bytes de una clase con una biblioteca externa como ASM .

En realidad, logré que mi ejemplo de “Hola mundo” trabajara con una expresión de reparto como

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

mediante el análisis del código de bytes de los métodos de llamada utilizando ASM. Pero el código es muy hacky e ineficiente, y uno probablemente nunca debería hacer algo como esto en el código de producción. De todos modos, solo para completar, aquí está el ejemplo completo de trabajo “Hola mundo”:

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

Un posible trabajo que podría ser de utilidad es definir interfaces vacías que extienden la interfaz que la lambda va a implementar y luego enviar a esta interfaz vacía solo para usar la anotación. Al igual que:

 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 extends Consumer{}; 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() ); } } } 

La anotación está disponible en las interfaces implementadas por la clase y es reutilizable si desea la misma anotación en otro lugar.

 public class Calculator { public static void main(String[] args) { try(Scanner scanner=new Scanner(System.in)){ System.out.println("Enter the operation to perform"); String key=scanner.next(); int i,j; switch (key) { case "Add": System.out.println("Input 2 values for addtion"); i =scanner.nextInt(); j=scanner.nextInt(); Calculate add=(Integer a, Integer b)-> a+b; System.out.println("After Addition of values="+add.calculate(i, j)); break; case "Multiply": System.out.println("Input 2 values for Multiplication"); i=scanner.nextInt(); j=scanner.nextInt(); Calculate multiplication=(a,b)->a*b; System.out.println("After Multiplication of values="+multiplication.calculate(i, j)); break; case "Subtract": System.out.println("Input 2 values for Subtraction"); i=scanner.nextInt(); j=scanner.nextInt(); Calculate subtract=(a,b)->ab; System.out.println("After Subtraction of values="+subtract.calculate(i, j)); break; case "Division": System.out.println("Input 2 values for Division"); i=scanner.nextInt(); j=scanner.nextInt(); Calculate division=(a,b)->a/b; if(j>0){ System.out.println("After Division of values="+division.calculate(i, j)); }else{ throw new Exception("Second value is 0. Please change the value"); } break; default: break; } }catch(Exception e){ e.printStackTrace(); } } } 

** Usar interfaz **

  @FunctionalInterface public interface Calculate { public T calculate(T a,T b); }