Tigerでのアノテーション 第2回: カスタム・アノテーション

Java 5で、独自のアノテーションを書く

このシリーズの第1回では、J2SE 5.0での新しいメタデータ機能であるアノテーションを紹介し、Tigerに組み込まれている基本的なアノテーションを説明しました。これに関連して、独自のアノテーションが書けるという、さらに強力な機能がサポートされています。今回の記事では、Brett McLauglinがカスタム・アノテーションの作り方を説明し、次にドキュメントへのアノテーションをどのように注釈付けし、さらなるコードのカスタム化をどう行うかを解説します。

Brett McLaughlin (brett@newInstance.com), Author/Editor, O'Reilly Media, Inc.

Author photoBrett McLaughlinはLogoの時代からコンピューターを扱ってきています (あの小さな三角形を憶えていますか・・・)。最近ではJavaやXMLコミュニティで最もよく知られた著作者兼プログラマーの一人として知られるようになりました。Nextel Communicationsでは複雑なエンタープライズ・システムを実装し、Lutris Technologiesではアプリケーション・サーバーを書き、また現在はで様々な書籍の著作編集活動を行っています。



2004年 9月 02日

このシリーズ第1回目の記事では、メタデータとは何か、なぜそれが貴重なのか、そしてJ2SE 5.0 (別名Tiger)で導入された、組み込みの基本アノテーションの使い方について説明しました。これらの概念に慣れてきた読者であれば、Java 5で提供されている3つの標準アノテーションがあまり堅牢なものではない、と思い始めているかも知れません。DeprecatedSuppressWarnings、それにOverrideだけでは、大したことはできません。幸いTigerでは、独自のアノテーション・タイプを定義できるようになっているのです。この記事では、この比較的単純なプロセスを、幾つかの例を挙げながら紹介して行きます。また、独自アノテーションに対してどのように注釈をつけるか、そうすることの利点は何かも学びます。ここで私はO'Reilly Media, Inc. に対して、Tigerに関する私の著書(参考文献)からコード例を引用することを承諾してくださったことに感謝致します。

独自のアノテーションを定義する

ちょっとした構文を追加したことにより(そしてTigerには構文的構成体(syntactical constructs)が山ほどあります)、Java言語は新しいタイプ、つまりアノテーション・タイプをサポートするようになっています。アノテーション・タイプは通常のクラスとよく似ていますが、幾つか独特の特徴を持っています。一番目につくのは、クラスの中で記号を付けて使うことで、他のJavaコードに注釈をつけることができる、という点です。では、このプロセスを順を追って説明しましょう。

@interface宣言

新しいアノテーション・タイプを定義するのは、新しいインターフェースを定義するのと非常に似ていますが、@記号を付けたinterfaceキーワードを前に付ける、という点が異なります。リスト1は、考えられるものとして最も単純なアノテーション・タイプを示しています。

リスト1. 非常に単純なアノテーション・タイプ
package com.oreilly.tiger.ch06;
/**
 * Marker annotation to indicate that a method or class
 *   is still in progress.
 */
public @interface InProgress { }

リスト1は、ごく自明でしょう。このアノテーション・タイプをコンパイルし、これが確実にクラスパス上にあるようにすれば、皆さんが自分のソースコード・メソッドに対してこれを使うことによって、メソッドあるいはクラスが、まだ進行中であることを示すことができます。これをリスト2に示します。

リスト2. カスタム・アノテーション・タイプを使う
@com.oreilly.tiger.ch06.InProgress
public void calculateInterest(float amount, float rate) {
  // Need to finish this method later
}

リスト1のアノテーション・タイプの使い方は、組み込みのアノテーション・タイプの使い方と全く同じですが、カスタム・アノテーションであることを、その名前とパッケージで示す必要があります。当然ですが、通常のJavaルールが適用されます。ですからアノテーション・タイプをインポートし、それを単純に@InProgressとして参照することができます。

他の記事もお忘れなく!

Java 5.0でのアノテーションを紹介した、このシリーズ「第1回」の記事も忘れずに読んでください。

数字を追加する

上で示した基本的な使い方は、とても堅牢なものとは言えません。第1回での説明を覚えていると思いますが、アノテーション・タイプはメンバー変数を持つことができます(参考文献)。これは、単なる生のドキュメンテーションではなく、より高度なメタデータとしてアノテーションを使い始めると、特に便利です。コード解析ツールは多くの情報を貪欲に要求するものですが、カスタム・アノテーションはそうした情報を提供できるのです。

アノテーション・タイプにおけるデータ・メンバーは、限定された情報を使って動作するように設定されています。メンバー変数を定義してからアクセサーやmutatorメソッドを提供したりはしません。メンバーの名前を付けた、考慮すべき単一のメソッドを定義するのです。データ型は、そのメソッドの戻り値とする必要があります。リスト3に示す具体的な例を見れば、ちょっと混乱しそうなこの要求事項を、明確に理解できるでしょう。

リスト3. アノテーション・タイプに数字を追加する
package com.oreilly.tiger.ch06;
/**
 * Annotation type to indicate a task still needs to be
 *   completed.
 */
public @interface TODO {
  String value();
}

リスト3は変に見えるかも知れませんが、これがアノテーション・タイプで必要なものなのです。リスト3では、アノテーション・タイプが受け付けるものとして、valueという名前の文字列を定義しています。その後で、リスト4に示すようにアノテーション・タイプを使うのです。

リスト4. メンバー値でアノテーション・タイプを使う
@com.oreilly.tiger.ch06.InProgress
@TODO("Figure out the amount of interest per month")
public void calculateInterest(float amount, float rate) {
  // Need to finish this method later
}

ここでも、特別なトリックはありません。リスト4はcom.oreilly.tiger.ch06.TODOがインポートされているものと想定しています。ですからソースの中では、アノテーションに対してパッケージ名での接頭辞は付けません。また、リスト4は簡略手法を使っていることにも注意してください。つまりメンバー変数の名前を規定せず、アノテーションの中に値("Figure out the amount of interest per month") を入れるのです。リスト5はリスト4と同じですが、簡略手法は使っていません。

リスト5. リスト4の「正式版」
@com.oreilly.tiger.ch06.InProgress
@TODO(value="Figure out the amount of interest per month")
public void calculateInterest(float amount, float rate) {
  // Need to finish this method later
}

当然ですが私達は皆コード屋なので、いったい誰が正式版を望んだりするのでしょうか? ただし、ちょっと注意してください。簡略版は、アノテーション・タイプが、valueという名前の単一メンバーの変数を持つ時にのみ利用可能なのです。この条件に合わない時には、簡略版は使えません。

デフォルト値を設定する

ここまでは、手始めには十分ですが、改善の余地が多分にあります。皆さんが恐らく次に追加したいのは、アノテーションにデフォルト値を設定することでしょう。これは、ユーザーが何かの値を規定できるように、しかもデフォルト値と異なる場合にのみ値を規定したい場合に便利です。リスト6は、この概念と、別のカスタム・アノテーションを使ってこれを実装した場合の両方を示しています。これは、リスト4で示したTODOアノテーション・タイプの、より完全な形です。

リスト6. デフォルト値を持つアノテーション・タイプ
package com.oreilly.tiger.ch06;
public @interface GroupTODO {
  public enum Severity { CRITICAL, IMPORTANT, TRIVIAL, DOCUMENTATION };
  Severity severity()default Severity.IMPORTANT;
  String item();
  String assignedTo();
  String dateAssigned();
}

リスト6のGroupTODOアノテーション・タイプでは、新しい変数を幾つか追加しています。このアノテーション・タイプは、単一メンバーの変数を持っていないことに注意してください。ですから変数の一つにvalueという名前を付けても、得るものは何もありません。一つ以上のメンバー変数を持つ場合には常に、できるだけ厳密に名前を付ける必要があります。ここではリスト5に示す簡略構文を使う利点はありませんので、もっと冗長にし、自分のアノテーション・タイプ用のセルフ・ドキュメントを作った方が良いかも知れません。

リスト6を見ると分かる、もう一つの特徴は、アノテーション・タイプは自分自身の列挙を定義する、という点です。(列挙(enumeration)は普通、単にenumとも言われますが、Java 5の新機能の一つです。ただしこれは驚くようなことではなく、アノテーション・タイプに特有なわけでもありません。)ということは、リスト6はメンバー変数のタイプとして、新しい列挙を使っていることになります。

最後に、本来の話題に戻って、デフォルト値です。デフォルト値を設定することは、ごく単純なことです。メンバー宣言の最後にキーワードdefaultを追加し、デフォルト値を与えるだけです。ご想像の通り、これは、メンバー変数に対して宣言したタイプと同じである必要があります。これも、特別高度な話ではありません。ちょっとした字句解析上の技なのです。リスト7は、GroupTODOアノテーションが実際に使われているところを示しています。この場合では、severityは示されていません。

リスト7. デフォルト値を利用する
  @com.oreilly.tiger.ch06.InProgress
  @GroupTODO(
    item="Figure out the amount of interest per month",
    assignedTo="Brett McLaughlin",
    dateAssigned="08/04/2004"
  )
  public  void calculateInterest(float amount, float rate) {
    // Need to finish this method later
  }

リスト8は同じアノテーションが使われていますが、この場合は、severityに値が与えられています。

リスト8. デフォルト値をオーバーライドする
  @com.oreilly.tiger.ch06.InProgress
  @GroupTODO(
    severity=GroupTODO.Severity.DOCUMENTATION,
    item="Need to explain how this rather unusual method works",
    assignedTo="Jon Stevens",
    dateAssigned="07/30/2004"
  )
  public  void reallyConfusingMethod(int codePoint) {
    // Really weird code implementation
  }

アノテーションに注釈を付ける

アノテーションについての説明を終わる前に、アノテーションへの注釈付けについて簡単に触れることにします。第1回で学んだ、事前定義された一連のアノテーション・タイプには、あらかじめ決められた目的があります。ところが皆さんが独自のアノテーション・タイプを書くようになると、そのアノテーションの目的は必ずしも自明とは限りません。皆さんは基本的なドキュメンテーションの他に、ある特定なメンバー・タイプ専用、あるいは、一連のメンバー・タイプ専用のタイプを書くかも知れません。そうした場合には、アノテーションで意図した機能をコンパイラーが強制できるように、アノテーション・タイプに対して何らかのメタデータを与える必要があります。

その解決方法として、アノテーションが、つまりJava言語でのメタデータとして選ばれたアノテーションが、まず頭に浮かぶはずです。事前定義された、メタ・アノテーションと呼ばれる4つのアノテーション・タイプを使って、アノテーションに注釈をつけることができるのです。この4つのそれぞれを順に、以下で説明しましょう。

ターゲットを規定する

最も分かりやすいメタ・アノテーションは、定義されたタイプのアノテーションを持てるのはどのプログラム要素かを規定できるものです。このメタ・アノテーションがTargetと呼ばれるのは、驚くに当たらないでしょう。ただし、Targetの使い方を見る前に、ElementTypeと呼ばれる、もう一つの新しいクラス(・・・実は列挙です)について知る必要があります。この列挙は、アノテーション・タイプのターゲットとなり得る、様々なプログラム要素を定義します。リスト9は、ElementType列挙の全体を示しています。

リスト9. ElementType列挙
package java.lang.annotation;
public enum ElementType {
  TYPE,     // Class, interface, or enum (but not annotation)
  FIELD,    // Field (including enumerated values)
  METHOD,   // Method (does not include constructors)
  PARAMETER,    // Method parameter
  CONSTRUCTOR,    // Constructor
  LOCAL_VARIABLE, // Local variable or catch clause
  ANNOTATION_TYPE,  // Annotation Types (meta-annotations)
  PACKAGE   // Java package
}

リスト9に示す列挙値は極めて明白で、それぞれがどのように適用されるかは(コメントを見れば)、皆さんも自分で判断できるでしょう。Targetメタ・アノテーションを使う場合には、こうした列挙値のうち少なくとも一つを与え、注釈をつけたアノテーションが、どのプログラム要素をターゲットとするかを指示します。リスト10は、Targetが実際に使われているところを示します。

リスト10. Targetメタ・アノテーションを使う
package com.oreilly.tiger.ch06;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
 * Annotation type to indicate a task still needs to be completed
 */
@Target({ElementType.TYPE,
         ElementType.METHOD,
         ElementType.CONSTRUCTOR,
         ElementType.ANNOTATION_TYPE})
public @interface TODO {
  String value();
}

これでJavaコンパイラーはTODOを、タイプとメソッド、コンストラクター、そしてその他のアノテーション・タイプに対してのみ適用します。こうすることによって、他の人が誰もそのアノテーション・タイプを誤用できないようにすることができます。(実はもっと良いのは、疲れから、皆さんが自分から誤用してしまうのを防げるという点です。)

retentionを設定する

手元に置いておきたい、次のメタ・アノテーションは、Retentionです。このメタ・アノテーションは、注釈付けされたアノテーションをJavaコンパイラーがどのように扱うかに関連しています。コンパイラーは幾つかのオプションを選択できます。

  • 注釈付けされたクラスの、コンパイル済みクラス・ファイルの中にアノテーションを保持し、そのクラスが最初にロードされる時に読み込む
  • コンパイル済みクラス・ファイルの中にアノテーションを保持するが、実行時には無視する
  • 指示された通りアノテーションを使うが、使った後でコンパイル済みクラス・ファイルの中に破棄する

この3つのオプションは、リスト11に示すjava.lang.annotation.RetentionPolicy列挙の中に表現されています。

リスト11. RetentionPolicy列挙
package java.lang.annotation;
public enum RetentionPolicy {
  SOURCE,   // Annotation is discarded by the compiler
  CLASS,    // Annotation is stored in the class file, but ignored by the VM
  RUNTIME   // Annotation is stored in the class file and read by the VM
}

ここまでで想像がつくかと思いますが、Retentionメタ・アノテーションは、その単一引き数として、リスト11に示す列挙値の一つを取ります。この、アノテーションへのメタ・アノテーションをターゲットにするのです。これをリスト12に示します。

リスト12. Retentionメタ・アノテーションを使う
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
  // annotation type body
}

リスト12から分かるように、Retentionは単一メンバーの変数を持っているので、ここでは簡略形式を使うことができます。そして、もしリテンションをRetentionPolicy.CLASSとしたい場合には、そうするための操作をする必要はありません。それがデフォルトの振る舞いだからです。

パブリックなドキュメンテーションを追加する

次のメタ・アノテーションはDocumentedです。Documentedはマーカー・アノテーションであることからも、簡単に理解できるものの一つです。第1回を覚えていると思いますが、マーカー・アノテーションには何もメンバー変数がありません。Documentedは、あるクラスに対するJavadocに、アノテーションが現れるはずであることを示します。デフォルトでは、アノテーションはJavadocに含まれません。これは、クラスの注釈付けに大きな時間をかける場合には、つまり今後何をすべきかを詳述したり、どんなことを正しく行うのか、あるいはその他の振る舞いをドキュメント化したりする場合に、覚えておくべき厳粛な事実です。

リスト13は、Documentedメタ・アノテーションが実際にどのように使われるかを示しています。

リスト13. Documentedメタ・アノテーションを使う
package com.oreilly.tiger.ch06;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
 * Marker annotation to indicate that a method or class
 *   is still in progress.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface InProgress { }

Documentedで「分かった!」と言えるのは、リテンションのポリシーでしょう。リスト13ではアノテーションのリテンションをRUNTIMEと規定していることに注意してください。これはDocumentedアノテーション・タイプを使う上で必須です。Javadocはその情報を、仮想マシンを使って、(ソースファイルではなく)クラス・ファイルからロードします。こうしたクラス・ファイルからJavadocを作るための情報を、仮想マシンが確実に得るようにするための唯一の方法は、RetentionPolicy.RUNTIMEのリテンションを規定することです。そうすれば、アノテーションはコンパイルされたクラス・ファイルの中に保持され、そして仮想マシンにロードされるようになります。するとJavadocはそれを拾い上げ、そのクラスのHTMLドキュメンテーションに追加します。

継承を設定する

最後のメタ・アノテーション、Inheritedは、恐らく実際に示すのが一番難しく、また一番使われることが少なく、また一番混乱を招きやすいものです。とは言っても、とにかく見てみることにしましょう。

まず、ユース・ケースを考えましょう。皆さんが自分のカスタムInProgressアノテーションで、進行中のクラスをマーク付けすることを想定してみてください。全く問題ありませんよね?正しくDocumentedメタ・アノテーションを適用していれば、これはJavadocにも現れてくるかも知れません。では、新しいクラスを書き、進行中のクラスを拡張すると考えてみてください。まったく簡単、ですよね? ただし、スーパークラスが進行中であることを思い出してください。サブクラスを使っても、そして、そのドキュメンテーションまで見ても、未完了であることを示すものが何もありません。InProgressアノテーションがサブクラスにまで伝わっていることが分かる、つまり継承されている、と期待したくなりますが、そうではないのです。リスト14に示すように、Inheritedメタ・アノテーションを使って、求める振る舞いを規定する必要があるのです。

リスト14. 継承されたメタ・アノテーションを使う
package com.oreilly.tiger.ch06;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
 * Marker annotation to indicate that a method or class
 *   is still in progress.
 */
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface InProgress { }

@Inheritedを追加することで、注釈付けされたクラスのサブクラスにInProgressアノテーションが現れてくるのが分かります。もちろん、すべてのアノテーション・タイプに対して、この振る舞いを望むことはないかもしれません(ですからデフォルトでは、継承しない、になっているのです)。例えばTODOアノテーションは伝達されません(そして伝達すべきではありません)。ただし、ここで示した場合について言えば、Inheritedは非常に便利です。


まとめ

ここまで来れば、Javaの世界に戻り、すべてをドキュメント化し、注釈付けする準備ができたことになります。そうすると私は、Javadocとは何かを、誰もが理解し始めた時のことを思い出してしまいます。誰もが皆、過剰にまで文書化するモードに入り込むうちに、やがて誰かが、Javadocが一番有効なのは、間違えやすいクラスやメソッドを明確にする時なのだ、と気がつくのです。つまり、簡単に理解できるgetXXX()setXXX()などのメソッドについては、いくら皆さんがJavadocに手をかけても、誰も見はしないのです。

アノテーションでも、程度は小さいかも知れませんが、同じことが恐らく起こるでしょう。標準のアノテーション・タイプを頻繁に、また、どんどん使うのは良い考えです。どんなJava 5コンパイラーでもそれをサポートしており。その振る舞いは誰でもよく理解しています。ところが、カスタム・アノテーションやメタ・アノテーションとなると、皆さんが懸命になって作ったタイプも、自分たちだけで閉じた開発環境の外に出た時に的確な意味を保つことが困難になります。ですから、よく注意してください。アノテーションは、使うことに意味がある時にだけ使いようにする必要がありますが、極端に考えることはありません。どのような使い方をするにせよ、アノテーションは持っていれば便利な機能であり、開発プロセスを助けてくれるのです。

参考文献

  • このシリーズ第1回の記事も忘れずに読んでください。Java 5.0でのアノテーションを紹介しています。
  • オープンソースのコード生成エンジン、XDocletを使うと、Java言語で属性指向のプログラミングができるようになります。
  • JSR 175はJava言語にメタデータ機能を採り入れるための仕様です。現在はJava Community Processで最終ドラフト提案の段階にあります。
  • Sunのホーム・ベースであるall things J2SE 5.0を見てください。
  • Tigerをダウンロードして、自分で試してみてください。
  • John Zukowskiによるタイガーを使いこなすシリーズは、Java 5.0の新しい機能を実際的なヒントを中心に説明しています。
  • Brett McLaughlinとDavid Flanagan共著によるJava 1.5 Tiger: A Developer's Notebook(2004年O'Reilly & Associates刊)はアノテーションを含めたTigerの最新機能のほぼ全てを、コード中心で開発者に分かりやすく網羅しています。
  • developerWorksのJava technologyゾーンにはJava技術に関する資料が豊富に取り揃えられています。
  • Developer BookstoreにはJava関連の書籍をはじめ、広範囲な話題を網羅した技術書が豊富に取り揃えられています。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218837
ArticleTitle=Tigerでのアノテーション 第2回: カスタム・アノテーション
publish-date=09022004