このエントリはJava Advent Calendar 2012の第19日目のエントリです。
昨日は @yoshioterada さんの「Concurrency Update (jsr166e)のご紹介」でした。
明日は @kis さんの「Javaでのパターンマッチを考える」です。
お二人に挟まれて、私は自分の少し恥ずかしい話をします。今日のテーマはアノテーションのインスタンスについてです。
私はある日、こういうメソッドに出くわしました。
select(Annotation...)
なるほどAnnotationクラスを引数として渡すのね、と初めは軽く考えてしまったのですが、実際にこのメソッドを呼ぼうとしたとき困りました。
最初に書いたコードはこうです。(笑
select(@Customized);
コンパイルエラーになりました。というかEclipseなので赤線が出ました。ここでアレっ?と思ったわけです。そこでコードを次のように直してみました。
select(Customized.class);
それでもやっぱり赤線が出ます。おや? メソッドが呼べない! もう大混乱です。
メソッドの定義がおかしいのでしょうか。いやいや、呼べないようなメソッドが定義されているはずはないので(なにしろJavaEEのAPIです)、私のコードが間違っている可能性の方が極めて高いです。頭を冷やしてよく考えてみましょう。
アノテーションでよく使うのはこういうコードですよね。
@Override public String toString() { // ... }
この@Overrideはアノテーションによる注釈付けをするときの書き方なわけです。
ではCustomized.classは・・・? 変数に代入してみるとよくわかります。
Class<Customized> type = Customized.class;
なるほど、Customized.classはCustomizedクラスを表すクラスインスタンスです。
とするとAnnotationクラスはどういった場面で登場するのでしょうか。こういうときは初心に帰って、Java docを開いてみます。java.lang.annotation.Annotationインターフェースの「使用」を見てみると、Class#getAnnotations()というメソッドの戻り値がAnnotation[]になっています。リフレクションですね。
@Customized public class ObtainAnnotation { public static void main(String[] args) { Annotation[] annotations = ObtainAnnotation.class.getAnnotations(); for (Annotation a : annotations) { System.out.println(a); } } }
@Customized()
ようやく分かってきました。アノテーションによって注釈付けされた要素に付いている、実際のアノテーションを表すのがAnnotationクラスのインスタンスです。
次に問題になるのは、アノテーションのインスタンスをいかに得るかということです。確かに注釈付けされた要素からリフレクションで得られることはわかりましたが、まさかいちいち注釈付け→リフレクションして取得するというのも奇妙な話です。
実際のところ、JavaEE6にはアノテーションのインスタンスを取得するためのクラス、AnnotationLiteral<T>が用意されています。具体的な使い方はProgrammatic CDIの方に書きましたので、よろしければ参照してみてください。
当然ながらそのクラスは、JavaEE環境でなければ(標準では)使用することができません。そこでJavaSE環境でアノテーションのインスタンスを取得するための方法を考えてみましょう。
まずは普通にnewしてみたらどうでしょうか。たぶんうまく行かない気がしますがやってみます。
Customized c = new Customized();
予想通り、これはコンパイルエラーになりました。
次に思いつくのはクラスからリフレクションで生成するという方法です。
public class ObtainAnnotation2 { public static void main(String[] args) { try { Customized c = Customized.class.newInstance(); System.out.println(c); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
java.lang.InstantiationException: Customized at java.lang.Class.newInstance0(Class.java:357) at java.lang.Class.newInstance(Class.java:325) at ObtainAnnotation2.main(ObtainAnnotation2.java:5)
例外が発生してしまいました。これでもやっぱりダメなようですね。メッセージには出ていませんが、該当行の辺りを見てみるとコンストラクタを取得するところで失敗しています。どういうことでしょう?
手がかりはアノテーションの定義にあります。アノテーションを定義するコードは、例えば以下のようなものですよね。
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Customized {
}
よく見てみると@interfaceと書いてあるではないですか。そうなんでした、アノテーションは特別なインターフェースなのです。直接インスタンス化できないのは当然です。
インターフェースと分かれば、道は見えました。実装を自分で書けばインスタンス化できるはずです。
public class CustomizedModifier implements Customized { private static final Class type = Customized.class; @Override public Class<? extends Annotation> annotationType() { return type; } @Override public boolean equals(Object o) { if (o == this) { return true; } if (!type.isInstance(o)) { return false; } return true; } @Override public int hashCode() { return 0; } } // ... Customized c = new CustomizedModifier();
これでコンパイルエラーは出なくなり、その代わりに見慣れない警告:"The annotation type Customized should not be used as a superinterface for CustomizedModifier"が出るようになりました。コンパイラが不審がるのももっともです。アノテーションは注釈をつけるための仕組みであるから、振る舞い(=実装)を持つべきではない、というわけです。
ちなみに@SuppressWarningsでこの警告のみを抑制する値を探したのですが、見つかりませんでした。コンパイラ次第ですが、"all"を使うしかないかもしれません。
ちょっと目先を変えて、お手本になりそうなものを見てみましょう。OpenJDKではアノテーションのインスタンスをどうやって作っているのでしょうか。OpenJDKのリフレクション部分のソースを追ってみると、sun.reflect.annotation.AnnotationParser内に実装がありました。以下該当メソッドの引用です。
public static Annotation annotationForMap( Class<? extends Annotation> type, Map<String, Object> memberValues) { return (Annotation) Proxy.newProxyInstance( type.getClassLoader(), new Class[] { type }, new AnnotationInvocationHandler(type, memberValues)); }
なるほど、Proxyを使っているんですね。リフレクションでは未知のアノテーションも扱わなければなりませんので、このようになっているのでしょう。うーむ、あまり参考になりませんでしたね。
残念ながら、すっきりした解決策は今のところありません。既知のアノテーションのインスタンスを取得するのであれば、@SuppressWarnings("all")のリスクがあるとしても、型安全を考えてそのアノテーションインターフェースを実装するのが良いと思います。
ただしアノテーションの正しい実装を書くことは、特にメンバーを持つアノテーションの場合、実はかなり骨の折れる作業です。java.lang.annotation.AnnotationのJava docにequals()やhashCode()の実装方法が規定されていますので、それを注意深く実装する必要があります。JavaEE6のCDIに用意されたAnnotationLiteral<T>は、その部分を実装してくれているのです。
そのCDIの参照実装であるJBoss Weldのコミュニティでも、AnnotationLiteral<T>の使用について疑問が呈されているのを目にしました。結論としては、Javaにアノテーションのリテラル記法が用意されていない以上、これ(AnnotationLiteral<T>の使用=アノテーションインターフェースの実装)が最善であるとされたようです。
そういえばリフレクションを使っていると、もどかしく感じることが多々ありますよね。
仮にこの4つが言語仕様に追加されたとすれば、リフレクションもかなり簡潔に書けるようになるのではないでしょうか。このうちパッケージリテラルは、どこかで導入が検討されている/いたと話に聞いたことがあるような・・・?