Java 8 présente à la fois Expressions Lambda et Annotations de type .
Avec les annotations de type, il est possible de définir Java annotations comme suit:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface MyTypeAnnotation {
public String value();
}
On peut alors utiliser cette annotation sur n'importe quelle référence de type comme par exemple:
Consumer<String> consumer = new @MyTypeAnnotation("Hello ") Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
};
Voici un exemple complet, qui utilise cette annotation pour imprimer "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);
}
}
}
La sortie sera:
Hello World!
Hello Type Annotations!
Dans Java 8, on peut également remplacer la classe anonyme dans cet exemple par une expression lambda:
public static void main(String[] args) {
List<String> list = Arrays.asList("World!", "Type Annotations!");
testTypeAnnotation(list, p -> System.out.println(p));
}
Mais comme le compilateur déduit l'argument de type Consumer pour l'expression lambda, on n'est plus en mesure d'annoter l'instance Consumer créée:
testTypeAnnotation(list, @MyTypeAnnotation("Hello ") (p -> System.out.println(p))); // Illegal!
On pourrait transtyper l'expression lambda dans un Consumer, puis annoter la référence de type de l'expression cast:
testTypeAnnotation(list,(@MyTypeAnnotation("Hello ") Consumer<String>) (p -> System.out.println(p))); // Legal!
Mais cela ne produira pas le résultat souhaité, car la classe Consumer créée ne sera pas annotée avec l'annotation de l'expression cast. Production:
World!
Type Annotations!
Deux questions:
Existe-t-il un moyen d'annoter une expression lambda similaire à l'annotation d'une classe anonyme correspondante, de sorte que l'on obtient la sortie "Hello World" attendue dans l'exemple ci-dessus?
Dans l'exemple, où j'ai casté l'expression lambda et annoté le type casté: existe-t-il un moyen de recevoir cette instance d'annotation au moment de l'exécution, ou une telle annotation est-elle toujours implicitement limitée à RetentionPolicy.SOURCE?
Les exemples ont été testés avec javac et le compilateur Eclipse.
Mise à jour
J'ai essayé la suggestion de @assylias, pour annoter le paramètre à la place, ce qui a produit un résultat intéressant. Voici la méthode de test mise à jour:
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);
}
}
Maintenant, on peut aussi produire le résultat "Hello World", en annotant le paramètre d'une classe anonyme:
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);
}
});
}
Mais l'annotation du paramètre ne fonctionne pas pas pour les 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));
}
Fait intéressant, il n'est également pas possible de recevoir le nom du paramètre (lors de la compilation avec le paramètre javac), lors de l'utilisation d'une expression lambda. Je ne suis pas sûr cependant, si ce comportement est prévu, si les annotations de paramètres de lambdas n'ont pas encore été implémentées, ou si cela devrait être considéré comme un bogue du compilateur.
Après avoir creusé dans la spécification finale Java SE 8 , je suis en mesure de répondre à mes questions.
(1) En réponse à ma première question
Existe-t-il un moyen d'annoter une expression lambda similaire à l'annotation d'une classe anonyme correspondante, de sorte que l'on obtient la sortie "Hello World" attendue dans l'exemple ci-dessus?
Non.
Lorsque vous annotez le Class Instance Creation Expression (§15.9)
d'un type anonyme, l'annotation sera stockée dans le fichier de classe pour l'interface d'extension ou la classe d'extension du type anonyme.
Pour l'annotation d'interface anonyme suivante
Consumer<String> c = new @MyTypeAnnotation("Hello ") Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
};
l'annotation de type est alors accessible à runtime en appelant Class#getAnnotatedInterfaces()
:
MyTypeAnnotation a = c.getClass().getAnnotatedInterfaces()[0].getAnnotation(MyTypeAnnotation.class);
Si vous créez une classe anonyme avec un corps vide comme celui-ci:
class MyClass implements Consumer<String>{
@Override
public void accept(String str) {
System.out.println(str);
}
}
Consumer<String> c = new @MyTypeAnnotation("Hello ") MyClass(){/*empty body!*/};
l'annotation de type est également accessible à runtime en appelant Class#getAnnotatedSuperclass()
:
MyTypeAnnotation a = c.getClass().getAnnotatedSuperclass().getAnnotation(MyTypeAnnotation.class);
Ce type d'annotation de type est pas possible pour les expressions lambda.
Sur une note latérale, ce type d'annotation n'est également pas possible pour les expressions de création d'instance de classe normales comme ceci:
Consumer<String> c = new @MyTypeAnnotation("Hello ") MyClass();
Dans ce cas, l'annotation de type sera stockée dans la structure method_info de la méthode, où l'expression s'est produite et non comme une annotation du type lui-même (ou l'un de ses super types ).
Cette différence est importante, car les annotations stockées dans method_info seront pas accessibles au moment de l'exécution par l'API de réflexion Java. Lorsque vous regardez le code d'octet généré avec [~ # ~] asm [~ # ~] , la différence ressemble à ceci:
Tapez Annotation sur une création d'instance d'interface anonyme:
@Java8Example$MyTypeAnnotation(value="Hello ") : CLASS_EXTENDS 0, null
// access flags 0x0
INNERCLASS Java8Example$1
Tapez Annotation sur une création d'instance de classe normale:
NEW Java8Example$MyClass
@Java8Example$MyTypeAnnotation(value="Hello ") : NEW, null
Alors que dans le premier cas, l'annotation est associée à classe interne, dans le second cas, l'annotation est associée à l'expression création d'instance à l'intérieur des méthodes code d'octet.
(2) En réponse au commentaire de @assylias
Vous pouvez également essayer (@MyTypeAnnotation ("Hello") String s) -> System.out.println (s) même si je n'ai pas réussi à accéder à la valeur d'annotation ...
Oui, cela est en fait possible selon la spécification Java 8. Mais il n'est actuellement pas possible de recevoir les annotations de type des paramètres formels des expressions lambda via l'API de réflexion Java, qui est probablement liée à ce bogue JDK: Type Annotations Cleanup . De plus, le compilateur Eclipse ne stocke pas encore l'attribut Runtime [In] VisibleTypeAnnotations correspondant dans le fichier de classe - le bogue correspondant se trouve ici: Les noms et annotations des paramètres Lambda ne parviennent pas aux fichiers de classe.
(3) En réponse à ma deuxième question
Dans l'exemple, où j'ai casté l'expression lambda et annoté le type casté: existe-t-il un moyen de recevoir cette instance d'annotation au moment de l'exécution, ou une telle annotation est-elle toujours implicitement limitée à RetentionPolicy.SOURCE?
Lors de l'annotation du type d'une expression cast, ces informations sont également stockées dans la structure method_info du fichier de classe. Il en va de même pour d'autres emplacements possibles d'annotations de type dans le code d'une méthode comme par ex. if(c instanceof @MyTypeAnnotation Consumer)
. Il n'existe actuellement aucune API de réflexion publique Java pour accéder à ces annotations de code. Mais puisqu'ils sont stockés dans le fichier de classe, il est au moins potentiellement possible d'y accéder au moment de l'exécution - par exemple en lisant le code d'octet d'une classe avec une bibliothèque externe comme [~ # ~] asm [~ # ~] .
En fait, j'ai réussi à faire fonctionner mon exemple "Hello World" avec une expression cast comme
testTypeAnnotation(list,(@MyTypeAnnotation("Hello ") Consumer<String>) (p -> System.out.println(p)));
en analysant le code d'octet des méthodes d'appel à l'aide d'ASM. Mais le code est très hacky et inefficace, et on ne devrait probablement jamais faire quelque chose comme ça dans le code de production. Quoi qu'il en soit, juste pour être complet, voici l'exemple complet de travail "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;
}
}
Une solution possible qui pourrait être utile consiste à définir des interfaces vides qui étendent l'interface que le lambda va implémenter, puis à transtyper en cette interface vide juste pour utiliser l'annotation. Ainsi:
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() );
}
}
}
L'annotation est alors disponible sur les interfaces implémentées par la classe et est réutilisable si vous souhaitez la même annotation ailleurs.