ネイティブアプリケーション開発お C 言語や C++ 言語を利用してきた技術者にとって、アノテーションは未知のものでしょう。 一方、Microsoft .NET の技術者にはアノテーションとは .NET における属性のことであると言えばそれだけで何かわかります。
アノテーションとは、クラスやメソッド、フィールドなどのプログラム要素に対して、プログラム的に意味づけを行うというものです。 ネイティブの開発者は、ソースコードにコメントを残したり、ドキュメントを作成するなどしてプログラム要素の意味を残しました。 しかし、これはプログラムコードと必ずしも同期しているわけではなく、ドキュメントがプログラムに対して古くなるというのは日常です。
新しい Java のアノテーションや、.NET の属性(アトリビュート)は、コンパイルされたプログラムの中に情報として意味づけされたオブジェクトを残します。 コメントと大きく異なる部分は、プログラム的に残されるため、プログラムから注釈情報などにアクセスできるという点です。 アノテーションから情報を取り出してドキュメントを自動生成するツールなどを容易に作ることができるようになります。
開発環境など、外部からコンパイルされたプログラムの内部にアクセスするリフレクションを経験していない技術者の場合、アノテーションの存在には戸惑いを感じるかもしれません。 アノテーションは、ソースコードに記述してオブジェクトと存在するにもかかわらず、プログラムの挙動にはかかわらないのです。 アノテーションを記述しても、それがプログラムの動作に関与するものではないのです。
アノテーションの代表的な使われ方は、クラスなどのプログラム部品を開発した開発者、開発企業、著作権情報などの埋め込みや、バージョン情報の埋め込みです。 こうすることで、挙動に直接かかわることなく、プログラムの中に情報を埋め込むことができるのです。 これらの情報は、ドキュメント生成ツールや開発環境などがリフレクション API から読み込んで利用することができます。
早速、アノテーションを作成して例を見てみましょう。 アノテーションを用いて、クラスやフィールド、メソッドに情報を追加するには、アノテーション型を宣言しなければなりません。 アノテーションの宣言は @interface キーワードを用います。
修飾子 @interface アノテーション型名 { アノテーション本体 }
アノテーション型の宣言は、クラスやインタフェースの宣言とほぼ同じです。 ただし、アノテーション型の実体は java.lang.annotation.Annotation インタフェースを継承するため extends で他の型を継承することはできません。
アノテーション本体にはメソッドを記述することができますが、これについては後述します。 まず、本体が何もない空のアノテーション型を宣言してみましょう。 本体が何も書かれていないアノテーションは、マーカーと呼ばれることもあります。
public @interface Test { }
アノテーションは実行時の挙動には関係しないため、この場は上記のようなアノテーション型を宣言してコンパイルしてください。 コンパイルしたプログラムは実行できませんが、今回はかまいません。 クラスファイルが作られれば、実行せずに javap コマンドで内部を調べてみましょう。
>javap Test Compiled from "Test.java" public interface Test extends java.lang.annotation.Annotation { }
このような結果が得られました。 アノテーション型をコンパイルした結果を見れば、その実体は java.lang.annotation.Annotation 型を継承するインタフェースにすぎないことが確認できます。
アノテーションは、それ自体が何らかの動作をするものではありません。 これをクラスやメソッド、フィールドなどに関連付けて初めて意味を持ちます。 新しい Java では、パッケージ、型、フィールド、メソッド、パラメータ、コンストラクタ、ローカル変数の宣言文の修飾子としてアノテーション型を指定することができます。 構文上、アノテーション型は新しい修飾子であると考えられ、public や static などと混在して記述することができますが、一般的な記法としてはアノテーションを他の修飾子よりも先に記述します。
@interface TestCode { } @TestCode public class Test { @TestCode private String name; public @TestCode Test(@TestCode String name) {} public static void main(String [] args) { @TestCode int i = 0; } }
このプログラムでは、宣言した TestCode アノテーションを Test クラス、name フィールド、Test コンストラクタ、Test コンストラクタの name パラメータ、main() メソッドの i ローカル変数に指定しています。 ただし、このプログラムの結果アノテーションが何らかの作用を及ぼすことはありません。 作られるプログラムは、何の変哲も無いクラスファイルです。
例えば、開発陣の中で TestCode アノテーションは「未実装である」または「お試しコードである」という意味を持つと定義します。 そうすれば、TestCode アノテーションを検索する開発支援ツールなどを作成することで、プロジェクトのなかで未実装のコードだけを列挙するというようなことが簡単になります。 アジャイル型など、とりあえずコードを書くという指向の開発チームはこうした手法をとることができるかもしれません。 このように、アノテーションはプログラムのソースコード、またはコンパイル結果のクラスファイルに情報を追加するというものなのです。
アノテーションはメソッドを宣言してメンバを保有することができます。 ただし、アノテーションの実体はインタフェースなので、メソッドの具体的な挙動を記述することはできません。 そもそも、アノテーションはプログラム的な注釈に過ぎないので、動作を持たせることに意味はありません。
アノテーションにメソッドが記述される場合、マーカー・アノテーションに対してメンバ・アノテーションと呼ばれます。 アノテーションに指定されているメソッドは、データを処理するためのものではなく、アノテーションに関連付けられている情報を返すためのメソッドとして機能します。
例えば、コードの著作権者を表すアノテーションの場合は、著作権者の名前を表す文字列を保有しなければ意味がありません。 同様に、バージョンであればバージョンナンバーを保持する必要があります。
メンバアノテーションの多くは、このように単一の注釈情報で十分であると考えられており、単一のメソッドだけが宣言されているアノテーションをシングル・メンバ・アノテーションと呼びます。 これに対して、複数のメソッドが宣言されているアノテーションをマルチ・メンバ・アノテーションと呼びます。
アノテーションで宣言されるメソッドは、インタフェース同様に抽象メソッドでなければなりません。 これに加えて、戻り値が値型、または String、Class、列挙型、アノテーション型のいずれか(またはその配列でもよい)でなければならず、パラメータを持つことはできないという規制があります。 さらに、メソッドは throws 節を持つことができず、型パラメータを持つこともできません。 つまり、単純にアノテーションが保有する情報を返すためだけのメソッド(get プロパティのようなもの)を宣言するのです。
@interface Copyright { String value(); }
上記のアノテーション型の宣言は、著作権者の情報を返すシングル・メンバ・アノテーションです。 アノテーションのメンバとして value() メソッドが記述されています。
このアノテーションでクラスやメソッドなどを修飾するには、アノテーションの型名に続いて ( ) でメンバの値を初期化します。
@アノテーション型 (メンバ名 = 定数)
これによって、アノテーションのメンバを初期化することができます。 アノテーションは実行時に操作されるものではないので、この情報はコンパイル時に決定します。 実行時にアノテーションのメンバを操作することはできず、アノテーションが持つ値は常に読み取り専用です。
@interface Copyright { String value(); } @Copyright(value = "LeonAkasaka") public class Test { public static void main(String [] args) { } }
このプログラムでは、著作権者情報を提供する Copyright アノテーションを宣言し、Test クラスの著作権者を "LeonAkasaka" で初期化しています。 Copyright アノテーションはメンバを保有しているため、Test クラスにアノテーションを指定したときに ( ) 内にアノテーションのメンバを定数で初期化させなければなりません。
シングル・メンバ・アノテーションの場合は、メソッドの名前を value にするのが礼儀です。 これには大きな理由があります。 アノテーションのメンバの値を初期化するには、上記のプログラムのように ( メンバ名 = 値 ) という形の代入式となります。 しかし、シングル・メンバ・アノテーションが一つのメンバしか保有していないのであれば、メンバ名を書かなくてもよいのではないかと考えることができます。
そこで、value() メソッドを一つしか持たないシングル・メンバ・アノテーションであれば、アノテーションを初期化するときにメンバ名を省略して直接定数を指定することができます。 value() 以外の名前であれば、メンバの値を指定するときにメンバ名を指定しなければなりません。
@interface Copyright { String value(); } @Copyright("LeonAkasaka") public class Test { public static void main(String [] args) { } }
このプログラムは、先ほどのプログラムと同じものですが、Test クラスに指定している Copyright アノテーションで、メンバ名を省略してメンバの値を指定していることがわかります。
マルチ・メンバ・アノテーションの場合は、複数のメソッドを持っているため、メンバの値を指定するにはカンマ , で区切って全てのメンバに定数を与えなければなりません。
@アノテーション型 ( メンバ名 = 定数 , メンバ名 = 定数 , ...)
シングル・メンバ・アノテーションは value() という名前のメソッドを宣言しますが、複数のメンバを持つアノテーションは、任意の名前のメソッドでかまいません。
@interface Copyright { String name(); int id(); } @Copyright(name = "LeonAkasaka", id = 1234) public class Test { public static void main(String [] args) { } }
このプログラムの Copyright アノテーションは、著作権者の名前を表す name メンバと、管理番号を表す id メンバがあるものとしています。 Test クラスにこのアノテーションを指定するときに、カンマ , で区切って全てのメンバにメンバ名 = 定数という形で初期値を与えなければなりません。