概要
Java5のアノテーションはとても強力なメタデータ機構を提供してくれます。ですが、どういう場面でそれを使えばよいかを理解する必要があるのは他の機能と同じです。この記事では、アノテーションが重要である理由と、その利用および誤用の事例について議論をしていきます。
メタデータの表現
それでは、私たちが古き良きJavaプログラマとして慣れ親しんできた事柄から始めることにしましょう。今、あるクラスのインスタンスがシリアライズされる可能性があるということを表現したいとします。私たちは次のようにSerializableインターフェースを実装することでこれを示すことができます。
public class MyClass implements java.io.Serializable { }
Serializableインターフェースにはいくつのメソッドが宣言されていると思いますか?答えは、言うまでもなくゼロです。メソッドをもたないインターフェースを準備してそれを実装することが一体何になるのでしょうか?私はこれをインヘリタンス・ハンマーを用いる、と言っています。私たちはここで何か振る舞いを得るためや特定の契約を表現するために継承を使っているのではありません。利用者が望むならそのクラスのオブジェクトをシリアライズできるということについて同意を与えるために使っているのです。私たちはSerializableインターフェースのことをタギングインターフェースと呼んでいます。Cloneableなどの他のインターフェースがタギングインターフェースに該当します。では、もしオブジェクトが何かフィールドをもっていて、そのフィールドだけはシリアライズしたくないとしたら?Javaではこれを表現するためにtransient キーワードを使用します。次のようになります。
public class MyClass implements java.io.Serializable
{
private int val1;
private transient int val2;
}
ということで、この例では私たちのやりたいことを実現するのにインターフェース(Serializable)とキーワード(transient)が必要でした。
この例についてさらに見ていきましょう。いくつかのサービスを提供してくれるあるフレームワークが私の手元にあると仮定してください。あなたは、自分で作成したクラスのオブジェクトをこの私のフレームワークに送信することができます。ですが、私はあなたのオブジェクトがスレッドセーフであるかどうかを知る必要があります。もしスレッドセーフでなければ、あなたは私がそのオブジェクトに複数スレッドから同時にアクセスすることを望まないでしょう。ここまで見てきた例に従って、私がタギングインターフェースをひとつ定義するという手があります(ThreadSafeインターフェースとしましょう)。あなたがこのインターフェースを実装してくれれば、私はあなたのクラスがスレッドセーフだと知ることができます。
public class MyClass
implements java.io.Serializable, VenkatsFramework.ThreadSafe
{
private int val1;
private transient int val2;
}
簡単ですね。では、あなたのこのクラスには何らかの理由で複数スレッドから呼び出すべきでないメソッドがあると仮定したら、私たちはどうすればよいでしょう?何も問題はありません。Javaに新しいキーワードを導入してくださいと誠意を込めてお願いすればいいのです。そうすれば、その新しいキーワードを使ってメソッドをマークできます(あるいはあなたはsynchronizedキーワードを利用すればよいと反論するかもしれませんが、キーワードを用いたアプローチの制限については理解してもらえることと思います)。
public class MyClass
implements java.io.Serializable, VenkatsFramework.ThreadSafe
{
private int val1;
private transient int val2;
public our_new_fangled_keyword void foo() // Not valid Java code
{
//...
}
}
ご覧のように、私たちにはメタデータを拡張する表現力が欠けています。私たちがやりたいことは、言ってみれば、クラスやメソッドやフィールドに印をつけて、それがシリアライズ可能であるとかスレッドセーフであるとか、あるいは必要に応じて表現したい何かであるということを示すということです。
さらなる力をあなたに
ここからはアノテーションです。アノテーションは新しいメタデータを使ってJava言語を拡張するすばらしい表現力を提供してくれます。これまでの例で記述した概念をどのように表現できるか、エレガントにアノテーションを使って見てみましょう。
//ThreadSafe.java
package VenkatsFramework;
public @interface ThreadSafe
{
}
//NotThreadSafe.java
package VenkatsFramework;
public @interface NotThreadSafe
{
}
//MyClass.java
package com.agiledeveloper;
import VenkatsFramework.ThreadSafe;
import VenkatsFramework.NotThreadSafe;
@ThreadSafe
public class MyClass implements java.io.Serializable
{
private int val1;
private transient int val2;
@NotThreadSafe public void foo()
{
//...
}
}
ThreadSafeアノテーションはまるでインターフェースのように記述されています(詳しくは後ほど説明します)。MyClassではThreadSafeアノテーション(クラスに適用)とNotThreadSafeアノテーション(メソッドに適用)を使いました。フレームワークを用いる場合、アノテーションは定義することよりも利用することのほうが多いでしょう。ですが、アノテーションをどのように定義するかを学ぶのは興味深いことです。
アノテーションを定義する
AuthorInfoというアノテーションを定義してみましょう。このアノテーションは誰がコードを書いたのかを注記するために利用できます。定義は次のようになります。
package com.agiledeveloper;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
public @interface AuthorInfo
{
public String name();
public String email() default "";
public String comment() default "";
}
AuthorInfoアノテーションはname、email、commentという三つの要素をもっています。これらはメソッド宣言に似たシンタックスで定義します。emailとcommentはデフォルト値をもっているので、@AuthorInfoを利用する際には省略することができます。アノテーションが値をひとつしかもたない場合、その値はメンバ名なしで指定することが可能です。
どこ(メソッド、クラス、フィールドなど)に@AuthorInfoアノテーションを用いることができるか?これは@Targetというメタアノテーション(アノテーションを注釈するアノテーション)を使って定義します。@Targetアノテーションに渡すことのできる有効なElementType値は、ANNOTATION_TYPE、CONSTRUCTOR、FIELD、LOCAL_VARIABLE、METHOD、PACKAGE、PARAMETER、TYPEです。@Inheritedメタアノテーションは、そのアノテーションが、宣言されたクラスだけでなくそのクラスから派生した全てのクラスに影響を与えるということを示します(AuthorInfoアノテーションの例で言うと、派生クラスの作者は基底クラスの作者とは異なるかもしれないので、Inheritedを使うのは理に適いません)。
最後に、@Retentionメタアノテーションはアノテーションがどの時点まで存在するかを指定します。値がRetentionPolicy.SOURCEであれば、アノテーションはソースコード中にはありますがコンパイラによって削除されます。RetentionPolicy.CLASSという値は、アノテーション情報がクラスファイル中にも保持されることを意味します。ですがVM上にはロードされません。RetentionPolicy.RUNTIMEという値は情報が実行時にも保持されることを意味し、リフレクションを使ってアノテーションの詳細を調べることが可能です。ここでは使っていないもうひとつのメタアノテーションに、アノテーションの使用をJavaDoc中にドキュメント化することを示す@Documentedがあります。
アノテーションを使う
AuthorInfoアノテーションの利用例を以下に示します。
package com.agiledeveloper;
@com.agiledeveloper.AuthorInfo(name = "Venkat Subramaniam")
public class SomeClass
{
@com.agiledeveloper.AuthorInfo(
name = "Venkat Subramaniam", comment = "bug free")
public void foo()
{
}
}
SomeClassにはname要素の値付きで@AuthorInfoアノテーションが指定されています。同様にfoo()メソッドにも@AuthorInfoアノテーションが用いられています。
次の例は正しくありません。
package com.agiledeveloper;
@com.agiledeveloper.AuthorInfo(name = "Venkat Subramaniam")
public class SomeClass2
{
// ERROR 'com.agiledeveloper.AuthorInfo' not applicable to fields
@com.agiledeveloper.AuthorInfo(name = "Venkat Subramaniam")
// Not valid
private int val1; }
@AuthorInfoはクラスとメソッドに対してのみ利用可能(@Targetアノテーションで定義されています)なので、コンパイラはエラーメッセージを出力します。
アノテーションを調べる
すでに述べたとおり、私たちはアノテーションを定義するよりも利用する場合がほとんどです。しかし、もしフレームワークがどのようにアノテーションを用いているかに興味があるなら、この項を読んでください。リフレクションを使って、クラスやメソッド、フィールドなどに宣言されているアノテーションの詳細を調べることができます。クラスに宣言された@AuthorInfoを調べるコード例を次に示します。
package com.agiledeveloper;
import java.lang.reflect.Method;
public class Example
{
public static void report(Class theClass)
{
if (theClass.isAnnotationPresent(AuthorInfo.class))
{
AuthorInfo authInfo =
(AuthorInfo) theClass.getAnnotation(AuthorInfo.class);
System.out.println(theClass.getName() +
" has @AuthorInfo annotation:");
System.out.printf("name = %s, email = %s, comment = %s\n",
authInfo.name(),
authInfo.email(), authInfo.comment());
}
else
{
System.out.println(theClass.getName()
+ " doesn't have @AuthorInfo annotation");
}
System.out.println("-----------------");
}
public static void main(String[] args)
{
report(SomeClass.class);
report(String.class);
}
}
このプログラムの出力は次のようになります。
com.agiledeveloper.SomeClass has @AuthorInfo annotation:
name = Venkat Subramaniam, email = , comment =
-----------------
java.lang.String doesn't have @AuthorInfo annotation
-----------------
ClassクラスのisAnnotationPresent()メソッドは、期待するアノテーションをクラスがもっているかどうかを教えてくれます。アノテーションの詳細を取得するにはgetAnnotation()メソッドを用います。
アノテーションの例
Java言語組み込みのアノテーションについて見てみましょう。
package com.agiledeveloper;
public class POJOClass
{
/**
* @deprecated Replaced by someOtherFoo()...
*/
public static void foo()
{
}
@Deprecated public static void foo1()
{
}
}
foo()メソッドは従来の方法を使ってメソッドを非推奨として宣言しています。このアプローチは表現力を欠いており、そしてSunのコンパイラは通常もし非推奨とされているメソッドを利用したら警告を発しますが、全てのコンパイラがこれについて警告を発してくれる保証はありません。より標準的でポータブルな方法は、次のように@Deprecatedを使うことです(ただ、以前のアプローチでは可能だった非推奨内容の記述能力に欠けていますが。なので通常は以前のアプローチと組み合わせて使うことになるでしょう)。
/**
* @deprecated Replaced by someOtherFoo1()...
*/
@Deprecated public static void foo1()
{
}
アノテーションとハンマー
「ハンマーしかもたない人には、全てがクギのように見える」という諺があります。アノテーションは優れたツールである一方、どんな場面でもそれを利用するのが正しいわけではありません。多くの人がXMLによる設定を嫌うようになっていますが、だからといって、XMLで設定しているものをいきなり全部アノテーションにしてしまうべきではありません。
本質的にコード内で表現したいことのためにアノテーションを使ってください。たとえば、Tapestryの@Persistアノテーションがよい例です。プログラマはBeanのプロパティを永続的であると宣言し、Tapestryはその保存を引き受けます(インスタンスのためのセッション内で)。同じことを伝えるのに冗長な設定を使うよりも、これをアノテーションとして定義するほうがずっとましです。どのみち、もし私がプロパティを永続化しないと決定したら、おそらくコード内の多くの箇所が変更されることになるでしょう。
もう一つのよい例を考えます。Webサービスを定義するにあたって、どのメソッドをWebサービスのメソッドとして外部に公開する必要があるのかをどうやって指定し、それをWSDL記述できるようにすればよいでしょうか?私たちは設定ファイルを使うソリューションに出くわしたことがあります。設定ファイルを使ったアプローチのひとつの問題は、コードを変更するとき(メソッド名の変更など)に、設定ファイルも変更しなければならないことです。その上、メソッドをサービスメソッドにしたりやめたりと頻繁に変更することは滅多にありません。メソッドをWebサービス用としてマーキングするのにアノテーションを使うのは理にかなっているかもしれません。
コード生成ツールは、アノテーションの表現力と言語のメタデータを拡張する機能によって、利用者が記述する特性に基づいてコードを作成できるようになります。アノテーションはアスペクトの記述にも役立ちます。
次にアノテーションを使ったメソッドのセキュリティ設定について考えます。これは無理な使い方でしょう。開発中にセキュリティの設定を変えるのは大いにあり得ることです。開発者はコード変更と再コンパイルなしに設定を変更できるようにしたいと思うかもしれません。外的な事柄を記述するのにアノテーションはベストな選択とは言えません。コードの外部に記述するのがベターです。それでは、膨大な量のXML設定を使えということなのでしょうか? いいえ、次のセクションで論じるとおり、その必要はないのです。
設定から規約へ
あるものはそれを設定項目としてアノテーションを使って表現するのがベターですし、またあるものはXMLやYAMLなどを使って外部に記述し、コードから分離するのがベターです。ですが、全ての項目を設定対象にすればよいというものではありません。設定可能にすれば柔軟性が増しますが、実生活と同様に、何事もあまりに多すぎるのは望ましくありません。ものによっては、設定よりも規約をベースにしたアプリケーションのほうが理解しやすいかもしれません。
たとえば、JUnit4.0以前のバージョンでは、あるメソッドがテストメソッドであることをtestというプレフィックスをつけることで示します。 JUnit4.0では、プレフィックスをつける代わりに、@Testというアノテーションを使ってメソッドをマーキングします。JUnit4.0のほかの機能や利点は考えないとして、アノテーションの利用は果たしてよいことでしょうか?もしかすると、アノテーションの利用から得られるメリットはテストクラスがTestCaseクラスを拡張しなくてもよいことだと、あなたは指摘するかもしれません。確かにそのとおりですが、でもそれはクラスレベルでの話です。メソッドレベルでは、ただ単に次のようにメソッドを書くだけでも、余計な箇所があるのではないでしょうか。
public void testMethod1() {}
または
@Test public void method1() {}
デフォルトでテストクラスの全てのメソッドをテストメソッドと見なせばよいのではないでしょうか。そして、メソッドがテストメソッドではないことを指定できるようにするのです(ここにアノテーションを使うかもしれません)。Javaでfinalを使わなければメソッドはバーチャルである(多態的であるということ)と見なされるのと似たような感じです。そうすれば、ノイズが減ってプログラマの意図を伝えやすくなると思いませんか?
規約を用いることが理にかなっていて、コード内の無駄な箇所に対する有益な情報の割合が増加し、コードの散乱やタイピング量が減るのなら、それは何も間違っていないということを、私は言いたいのです。
アノテーションを使うか、使わないか
アノテーションを使うタイミングは興味深い問題です。「いつでも」という答えも、「決して使うべきではない」という答えも、正しくありません。アノテーションの使用が適していて、もっと言えばエレガントでさえある場所というものがあるのです。一方で、ベストな選択とは言えないような場所もあります。
次のような場合にアノテーションを使いましょう
- メタデータが本質的な情報であるもの
- もし何か使いたいキーワードが言語によって提供されているなら、それはアノテーションの候補になり得る(たとえばtransient)
- アノテーションのほうが他の方法よりも表現的にシンプルであり、作業がしやすいもの
- たとえば、WebサービスにおいてメソッドをWebメソッドとしてマーキングするのは、同じことをXMLの設定ファイルに書くよりも簡単
- クラスベースであり、オブジェクトごとに特有ではないもの
- クラスの個別のオブジェクト単位ではなく、クラス単位で記述されるメタデータ
次のような場合は使うべきではありません
- 現在XMLで設定しているというだけでは、それをアノテーションに置き換える理由にはならない
- XMLの設定情報を盲目的にアノテーションに置き換えないこと
- すでにエレガントな方法で指定を行っている場合
- 情報を表現している手段がすでにエレガントで、あなたがそれに満足しているなら、アノテーションを使う圧力の下に身をおく必要はない
- メタデータが、値を柔軟に変更できることを目的としたものである場合
- たとえばいつでも好きなときに変更したいBeanのメソッドのセキュリティ設定など、項目を変更または設定可能にしたい場合は、設定ファイルの中に置いておくのが賢明
- アプリケーションが設定の代わりに規約を用いることで詳細を理解可能である場合
- 明白な、もしくはデフォルトの値と異なるものだけ設定すればよい(設定にはアノテーションか別の手段を用いる)
終わりに
アノテーションは興味深い追加機能です。新しいメタデータを使ってJava言語を拡張する協力な仕組みを提供してくれます。ですが、状況に応じてそのメリットをじっくり評価しなくてはなりません。「正しいアノテーションの使い方」はアプリケーション開発において当然考慮されるべき設計上の関心事です。盲目的な承認も、盲目的な拒絶も、望ましくありません。
著者紹介
Dr. Venkat SubramaniamはAgile Developer, Inc. の創設者である。アメリカ・カナダ・ヨーロッパ・インドで3,000人を超えるソフトウェア技術者を指導、アドバイスしてきた。クライアントがプロジェクトでアジャイル開発を効果的に適用して成功を収めるのをサポートし、頻繁にカンファレンスで講演を行っている。また、ヒューストン大学の非常勤教員を務め、ライス大学ではプロフェッショナルなソフトウェア開発者シリーズの教鞭をとっており、絶えず研究を続けている。『.NET Gotchas』の著者で、『Practices of an Agile Developer』の共著者でもある。
原文はこちらです:http://www.infoq.com/articles/Annotation-Hammer
(このArticleは2006年7月25日にリリースされました)