1. 概要

最近では、Java言語の強力なツールである注釈なしのJavaを想像するのは困難です。

Javaは、組み込みアノテーションのセットを提供します。 さらに、さまざまなライブラリからの注釈がたくさんあります。 独自の注釈を定義して処理することもできます。 これらの注釈は属性値で調整できますが、これらの属性値には制限があります。 特に、アノテーション属性値は定数式である必要があります。

このチュートリアルでは、その制限のいくつかの理由を学び、JVMの内部を調べてそれをよりよく説明します。 また、アノテーション属性値に関連する問題と解決策の例をいくつか見ていきます。

2. 内部のJavaアノテーション属性

Javaクラスファイルがどのように注釈属性を格納するかを考えてみましょう。 Javaには、element_valueと呼ばれる特別な構造があります。 この構造は、特定の注釈属性を格納します。

構造体element_valueは、次の4つの異なるタイプの値を格納できます。

  • 定数のプールからの定数
  • クラスリテラル
  • ネストされた注釈
  • 値の配列

したがって、アノテーション属性の定数はコンパイル時定数です。 そうしないと、コンパイラーは定数プールに入れて注釈属性として使用する値を認識できません。

Java仕様では、定数式を生成する操作が定義されています。 これらの操作をコンパイル時定数に適用すると、コンパイル時定数が取得されます。

属性valueを持つ注釈@Markerがあると仮定します。

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

たとえば、このコードはエラーなしでコンパイルされます。

@Marker(Example.ATTRIBUTE_FOO + Example.ATTRIBUTE_BAR)
public class Example {
    static final String ATTRIBUTE_FOO = "foo";
    static final String ATTRIBUTE_BAR = "bar";

    // ...
}

ここでは、注釈属性を2つの文字列の連結として定義します。 連結演算子は定数式を生成します。

3. 静的イニシャライザーの使用

staticブロックで初期化された定数を考えてみましょう。

@Marker(Example.ATTRIBUTE_FOO)
public class Example {
    static final String[] ATTRIBUTES = {"foo", "Bar"};
    static final String ATTRIBUTE_FOO;

    static {
        ATTRIBUTE_FOO = ATTRIBUTES[0];
    }
    
    // ...
}

static ブロックのフィールドを初期化し、そのフィールドを注釈属性として使用しようとします。 このアプローチはコンパイルエラーにつながります。

まず、変数ATTRIBUTE_FOOにはstaticおよびfinal修飾子がありますが、コンパイラーはそのフィールドを計算できません。 アプリケーションは実行時にそれを計算します。

次に、アノテーション属性は、JVMがクラスをロードする前に正確な値を持っている必要があります。 ただし、 static 初期化子を実行すると、クラスはすでにロードされています。 したがって、この制限は理にかなっています。

フィールドの初期化時に同じエラーが表示されます。 同じ理由で、このコードは正しくありません。

@Marker(Example.ATTRIBUTE_FOO)
public class Example {
    static final String[] ATTRIBUTES = {"foo", "Bar"};
    static final String ATTRIBUTE_FOO = ATTRIBUTES[0];

    // ...
}

JVMはどのようにATTRIBUTE_FOOを初期化しますか? 配列アクセス演算子ATTRIBUTES[0] は、クラス初期化子で実行されます。 したがって、ATTRIBUTE_FOOは実行時定数です。 コンパイル時には定義されません。

4. 注釈属性としての配列定数

配列注釈属性について考えてみましょう。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Marker {
    String[] value();
}

このコードはコンパイルされません:

@Marker(value = Example.ATTRIBUTES)
public class Example {
    static final String[] ATTRIBUTES = {"foo", "bar"};

    // ...
}

まず、 final 修飾子は参照が変更されないように保護しますが、配列要素を変更することはできます。

2番、 配列リテラルを実行時定数にすることはできません。 JVMは、静的初期化子で各要素を設定します —前述の制限。

最後に、クラスファイルには、その配列の各要素の値が格納されます。 したがって、コンパイラは属性配列の各要素を計算し、コンパイル時に実行されます。

したがって、毎回配列属性を指定することしかできません。

@Marker(value = {"foo", "bar"})
public class Example {
    // ...
}

定数は、配列属性のプリミティブ要素として引き続き使用できます。

5. マーカーインターフェイスの注釈:なぜ機能しないのですか?

したがって、注釈属性が配列の場合、毎回それを繰り返す必要があります。 ただし、このコピー&ペーストは避けたいと思います。 アノテーション@AliExpressを作成してみませんか? マーカーインターフェイスに注釈を追加できます。

@Marker(value = {"foo", "bar"})
public interface MarkerInterface {
}

次に、このアノテーションを必要とするクラスにそれを実装させることができます。

public class Example implements MarkerInterface {
    // ...
}

このアプローチは機能しません。 コードはエラーなしでコンパイルされます。 ただし、 Javaは、アノテーション自体に @AliExpress アノテーションが含まれている場合でも、interfacesからのアノテーション継承をサポートしていません。 したがって、マーカーインターフェイスを実装するクラスはアノテーションを継承しません。

この理由は多重継承の問題です。 実際、複数のインターフェースに同じ注釈がある場合、Javaは1つを選択できません。

したがって、このコピーアンドペーストをマーカーインターフェイスで回避することはできません。

6. 注釈属性としての配列要素

配列定数があり、この定数を注釈属性として使用するとします。

@Marker(Example.ATTRIBUTES[0])
public class Example {
    static final String[] ATTRIBUTES = {"Foo", "Bar"};
    // ...
}

このコードはコンパイルされません。 アノテーションパラメータはコンパイル時定数でなければなりません。 ただし、前に検討したように、配列はコンパイル時定数ではありません。

また、配列アクセス式は定数式ではありません。

配列の代わりにListがある場合はどうなりますか? メソッド呼び出しは定数式に属していません。 したがって、Listクラスのgetメソッドを使用すると、同じエラーが発生します。

代わりに、定数を明示的に参照する必要があります。

@Marker(Example.ATTRIBUTE_FOO)
public class Example {
    static final String ATTRIBUTE_FOO = "Foo";
    static final String[] ATTRIBUTES = {ATTRIBUTE_FOO, "Bar"};
    // ...
}

このように、文字列定数で注釈属性値を指定すると、Javaコンパイラーは属性値を明確に見つけることができます。

7. 結論

この記事では、アノテーションパラメータの制限について説明しました。 アノテーション属性に関する問題の例をいくつか検討しました。 また、これらの制限のコンテキストでJVM内部についても説明しました。

すべての例で、定数とアノテーションに同じクラスを使用しました。 ただし、これらの制限はすべて、定数が別のクラスからのものである場合にも当てはまります。