☕️

Javaのレコードについてメモ:アノテーション

2021/09/18に公開

レコードについて軽く調べてメモっておくシリーズ。
今回はアノテーションについてです。

環境

$ java --version
java 17 2021-09-14 LTS
Java(TM) SE Runtime Environment (build 17+35-LTS-2724)
Java HotSpot(TM) 64-Bit Server VM (build 17+35-LTS-2724, mixed mode, sharing)

アノテーション付与パターン

コンポーネントの宣言にアノテーションを付けるとCanonical Constructorの引数とフィールド、アクセサーメソッドのすべてに同じアノテーションが付与されるようです。

アノテーションが付与される箇所をコントロールしたい場合は次のようにすると良さそうです。

ケース コンストラクタ引数 フィールド アクセサー 方法
1 コンポーネントの宣言にアノテーションを付ける
2 - コンポーネントの宣言にアノテーションを付けてアクセサーを明示的に宣言する(アクセサーにはアノテーションを付けない)
3 - コンストラクターとアクセサーを明示的に宣言してコンストラクター引数とアクセサーにアノテーションを付ける
4 - コンポーネントの宣言にアノテーションを付けてコンストラクターを明示的に宣言する(コンストラクター引数にはアノテーションを付けない)
5 - - コンストラクターを明示的に宣言してコンストラクター引数にアノテーションを付ける
6 - - コンポーネントの宣言にアノテーションを付けてコンストラクターとアクセサーを明示的に宣言する(コンストラクター引数とアクセサーにはアノテーションを付けない)
7 - - アクセサーを明示的に宣言してアノテーションを付ける
アノテーション付与パターンの確認用コード
package example.record;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Comparator;

public class RecordAnnotationPatternDemo {

    record Case1(@Foobar String component) {
    }

    record Case2(@Foobar String component) {
        public String component() {
            return component;
        }
    }

    record Case3(String component) {
        Case3(@Foobar String component) {
            this.component = component;
        }

        @Foobar
        public String component() {
            return component;
        }
    }

    record Case4(@Foobar String component) {
        Case4(String component) {
            this.component = component;
        }
    }

    record Case5(String component) {
        Case5(@Foobar String component) {
            this.component = component;
        }
    }

    record Case6(@Foobar String component) {
        Case6(String component) {
            this.component = component;
        }

        public String component() {
            return component;
        }
    }

    record Case7(String component) {
        @Foobar
        public String component() {
            return component;
        }
    }

    @Retention(RetentionPolicy.RUNTIME)
    @interface Foobar {
    }

    public static void main(String[] args) throws ReflectiveOperationException {
        for (Class<?> clazz : Arrays.stream(RecordAnnotationPatternDemo.class.getDeclaredClasses())
                .filter(Class::isRecord)
                .sorted(Comparator.comparing(Class::getSimpleName))
                .toList()) {
            inspect(clazz);
        }
    }

    static void inspect(Class<?> clazz) throws ReflectiveOperationException {
        String componentName = "component";
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
        Parameter constructorParameter = constructor.getParameters()[0];
        Field field = clazz.getDeclaredField(componentName);
        Method accessor = clazz.getDeclaredMethod(componentName);
        System.out.printf("%s %s %s %s%n", clazz.getSimpleName(),
                hasAnnotation(constructorParameter),
                hasAnnotation(field),
                hasAnnotation(accessor));
    }

    static String hasAnnotation(AnnotatedElement element) {
        return element.getDeclaredAnnotation(Foobar.class) != null ? "o" : "-";
    }
}

アノテーションの@TargetとElementType

アノテーションは@Targetで付与できる対象(ElementType)を設定できます。

レコードのコンポーネントに付与するアノテーションは次のElementTypeを対象にできるようです。

  • RECORD_COMPONENT
  • PARAMETER
  • FIELD
  • METHOD
  • TYPE_USE

これらのElementType@Targetに持つアノテーションを5つ用意し、それぞれコンポーネントに付与したレコードをリフレクションで調べてみました。
そうするとPARAMETER/FIELD/METHODはそれぞれコンストラクター引数/フィールド/アクセサーメソッドにアノテーションが付いていました。
RECORD_COMPONENT(とTYPE_USE)はそれらにアノテーションは付いていませんでした。

ただし、ClassクラスにgetRecordComponentsというメソッドが追加されており、それで得たjava.lang.reflect.RecordComponentからはアノテーションを取得できました。
RecordComponentからはアクセサーメソッドも取得できるみたいです。
なるほど。

ElementTypeとアノテーション付与の確認用コード
package example.record;

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.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.RecordComponent;
import java.util.Arrays;
import java.util.Comparator;

public record RecordAnnotationTargetDemo(@Case1 @Case2 @Case3 @Case4 @Case5 String component) {

    @Target({ ElementType.RECORD_COMPONENT })
    @Retention(RetentionPolicy.RUNTIME)
    @interface Case1 {
    }

    @Target({ ElementType.PARAMETER })
    @Retention(RetentionPolicy.RUNTIME)
    @interface Case2 {
    }

    @Target({ ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    @interface Case3 {
    }

    @Target({ ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @interface Case4 {
    }

    @Target({ ElementType.TYPE_USE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface Case5 {
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws ReflectiveOperationException {
        for (Class<?> clazz : Arrays.stream(RecordAnnotationTargetDemo.class.getDeclaredClasses())
                .filter(Class::isAnnotation)
                .sorted(Comparator.comparing(Class::getSimpleName))
                .toList()) {
            inspect((Class<? extends Annotation>) clazz);
        }
    }

    static void inspect(Class<? extends Annotation> annotation) throws ReflectiveOperationException {
        Class<?> clazz = RecordAnnotationTargetDemo.class;
        RecordComponent component = clazz.getRecordComponents()[0];
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
        Parameter constructorParameter = constructor.getParameters()[0];
        Field field = clazz.getDeclaredField(component.getName());
        Method accessor = component.getAccessor();
        System.out.printf("%s %s %s %s %s%n", annotation.getSimpleName(),
                hasAnnotation(annotation, component),
                hasAnnotation(annotation, constructorParameter),
                hasAnnotation(annotation, field),
                hasAnnotation(annotation, accessor));
    }

    static String hasAnnotation(Class<? extends Annotation> annotation, AnnotatedElement element) {
        return element.getDeclaredAnnotation(annotation) != null ? "o" : "-";
    }
}

以上です。

Discussion