ラムダ式の関数インターフェースに注釈を付ける 質問する

ラムダ式の関数インターフェースに注釈を付ける 質問する

Java 8では、ラムダ式そして型注釈

型注釈を使用すると、次のような Java 注釈を定義できます。

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

次に、このアノテーションを任意の型参照に使用できます。例:

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

以下は、この注釈を使用して「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);
        }
    }
}

出力は次のようになります。

Hello World! 
Hello Type Annotations!

Java 8 では、この例の匿名クラスをラムダ式に置き換えることもできます。

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

しかし、コンパイラはラムダ式の Consumer 型引数を推論するため、作成された Consumer インスタンスに注釈を付けることができなくなります。

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

ラムダ式を Consumer にキャストし、キャスト式の型参照に注釈を付けることができます。

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

しかし、作成された Consumer クラスにはキャスト式の注釈が付けられないため、これでは望ましい結果は得られません。出力:

World!
Type Annotations!

2つの質問:

  1. 対応する匿名クラスに注釈を付けるのと同様にラムダ式に注釈を付けて、上記の例で期待される「Hello World」出力を取得する方法はありますか?

  2. この例では、ラムダ式をキャストし、キャストされた型に注釈を付けました。実行時にこの注釈インスタンスを受け取る方法はありますか、それとも、このような注釈は常に暗黙的に RetentionPolicy.SOURCE に制限されますか?

これらの例は、javac と Eclipse コンパイラを使用してテストされています。

アップデート

代わりにパラメータに注釈を付けるという @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);
    }
}

匿名クラスのパラメータに注釈を付けるときに、「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);
        }
    });
}

しかし、パラメータに注釈を付けるとないラムダ式で動作します:

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

興味深いことに、ラムダ式を使用する場合、パラメータの名前を受け取ることもできません (javac -parameter でコンパイルする場合)。ただし、この動作が意図されたものなのか、ラムダのパラメータ注釈がまだ実装されていないのか、またはこれがコンパイラのバグと見なされるべきなのかはわかりません。

ベストアンサー1

掘り下げてみるとJava SE 8 最終仕様私は自分の質問に答えることができます。

(1)私の最初の質問に対する回答

対応する匿名クラスに注釈を付けるのと同様にラムダ式に注釈を付けて、上記の例で期待される「Hello World」出力を取得する方法はありますか?

いいえ。

匿名型の に注釈を付ける場合Class Instance Creation Expression (§15.9)、その注釈は匿名型の拡張インターフェースまたは拡張クラスのいずれかのクラス ファイルに保存されます。

次の匿名インターフェースアノテーションの場合

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

型注釈は次のようにアクセスすることができます。ランタイム電話をかけるClass#getAnnotatedInterfaces():

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

次のように空の本体を持つ匿名クラスを作成する場合:

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

型注釈は以下からもアクセスできます。ランタイム電話をかけるClass#getAnnotatedSuperclass():

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

この種の型注釈はないラムダ式では可能です。

ちなみに、この種の注釈は、次のような通常のクラスインスタンス作成式では不可能です。

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

この場合、型注釈はmethod_info 構造型自体 (またはそのスーパー タイプのいずれか) の注釈としてではなく、式が発生したメソッドの注釈として使用されます。

この違いは重要です。なぜなら、method_infoに保存された注釈はないJavaリフレクションAPIによって実行時にアクセス可能。生成されたバイトコードをオーストラリア違いは次のようになります。

匿名インターフェースインスタンス作成時の型注釈:

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

通常のクラスインスタンス作成時の型注釈:

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

最初のケースでは、注釈は内部クラス2番目のケースでは、注釈はインスタンスの作成メソッドのバイトコード内の式。

(2)@assyliasからのコメントへの返答

注釈値にアクセスすることはできませんでしたが、 (@MyTypeAnnotation("Hello ") String s) -> System.out.println(s) を試すこともできます...

はい、これは Java 8 仕様によれば実際に可能です。ただし、現時点では、ラムダ式の仮パラメータの型注釈を Java リフレクション API 経由で受け取ることはできません。これは、おそらくこの JDK のバグに関連しています。型注釈のクリーンアップまた、Eclipse コンパイラーはまだ関連する Runtime[In]VisibleTypeAnnotations 属性をクラス ファイルに保存していません。対応するバグはここにあります:Lambda パラメータ名と注釈はクラス ファイルには含まれません。

(3)私の2番目の質問に対する回答

この例では、ラムダ式をキャストし、キャストされた型に注釈を付けました。実行時にこの注釈インスタンスを受け取る方法はありますか、それとも、このような注釈は常に暗黙的に RetentionPolicy.SOURCE に制限されますか?

キャスト式の型を注釈する場合、この情報はクラスファイルの method_info 構造にも保存されます。これは、たとえばメソッドのコード内の型注釈のその他の可能な場所にも当てはまりますif(c instanceof @MyTypeAnnotation Consumer)。現在、これらのコード注釈にアクセスするための公開 Java リフレクション API はありません。ただし、これらはクラスファイルに保存されるため、実行時に少なくとも潜在的にアクセスできます。たとえば、次のような外部ライブラリを使用してクラスのバイトコードを読み取ることによってアクセスできます。オーストラリア

実際、私は「Hello World」の例を次のようなキャスト式で動作させることができました。

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

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;
    }
}

おすすめ記事