Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

Metadata は魔法の言葉

 
 
Tiger Metadata とはなんなのだろう
 
 

Metadata ってよくわからない言葉ですね。データ自身の情報をあらわすための情報とでもいえばいいのでしょうか。

たとえば、JDBC の ResultSet にはメタデータを表すための ResultSetMetadata というクラスがあります。ResultSet クラスが実際にデータベースから取ってきた情報を保持しているのに対し、ResultSetMetadata クラスはカラムの名前や型など ResultSet クラスが保持する情報を説明するための情報を保持しています。

このように Metadata は情報自体を説明するための情報ということがいえます。情報の注釈といってもいいと思います。

それではなぜ今そんな情報をあつかえるようになったのでしょう。

プログラミングをしていると、プログラムロジックには関係のないコーディングというのが少なからず存在します。たとえば、RMI を使用するときに Remote インタフェースを派生させたインタフェースを書く必要があります。

同様に JAX-RPC も Web Services として公開するためには、Remote インタフェースを派生させたインタフェースを書く必要があります。Web Services の場合は WSDL も書かなくてはいけません。

しかし、このような作業は定型処理であり、できれば自動化したいのが人情です。

そこで、役立つのが Metadata です。Metadata を使用して Web Services として公開したいメソッドに注釈をつけておきます。そうすれば、たとえば JAX-RPC の wscompile などのようなツールが Metadata を読み込んで、自動でインタフェースや WSDL を作成することができるようになるかもしれないのです。

実をいうと、Tiger だけでは Metadata の利点はあまりありません。Metadata を扱うことのできるツールやライブラリが充実してこないことには Metadata のおいしさが味わえないのです。

しかし、J2EE 1.5 では多くのライブラリやツールが Metadata をサポートすることが発表されています。一例をあげると

  • Deployment Descriptors の作成ツール
  • EJB デフォルトの設定など
  • JAX-RPC インタフェースや WSDL の自動生成
  • JAXB 2.0
  • JDBC 4.0

J2EE 1.5 が登場するのはずいぶん先になると思いますが、今のうちに Metadata になじんでおけば J2EE 1.5 も怖くありませんね。

Metadata は他の Tiger の新機能と同様に JCP で仕様策定されています。JSR-175 になります。仕様などは そこで見ることができます。逆に J2SDK beta のドキュメントには Metadata についての記述はほとんどないので (もちろん JavaDoc はありますが)、JSR-175 を参照する必要があります。

それでは、さっそく Metadata に触れてみましょう。

 

 
 
Tiger とりあえず使ってみよう
 
 

Metadata を記述するにはアノテーションを使用します。習うより慣れろということで、次のソースをご覧ください。

サンプルのソース AnnotationSample.java
AnnotationSampleClient.java
Metadata を定義している JAR ファイル logannotation.jar

かなり作為的なソースですがそれはおいておいて、@Log というのが Metadata というかアノテーションになります。

import jp.gr.java_conf.skrb.annotation.Log;
import jp.gr.java_conf.skrb.annotation.LogLevel;
 
public class AnnotationSample {
    @Log
    public AnnotationSample() {}
 
    @Log
    public AnnotationSample(String title, boolean flag) 
                            throws IllegalArgumentException {}
 
    @Log
    public void foo(String msg, int x) throws Exception {
        System.out.println("Calls foo(" + msg + ", " + x + ")");
    }
 
    @Log
    public int bar(double x) {
        System.out.println("Calls bar(" + x + ")");
        return (int)x;
    }
 
    public int bar() {
        System.out.println("Calls bar()");
        return 0;
    }
}

そして、この AnnotationSample クラスを使用するのが、AnnotationSmapleClient クラスです。

public class AnnotationSampleClient {
    public AnnotationSampleClient() throws Exception {
        AnnotationSample sample = new AnnotationSample();
        sample = new AnnotationSample("test", true);
 
        sample.foo("calls foo", 10);
        sample.bar(1.0);
        sample.bar();
    }
 
    public static void main(String[] args) throws Exception {
        new AnnotationSampleClient();
    }
}

これをコンパイル、実行してみましょう。javac を実行している行は 2 行にわたっていますが、実際には 1 行です。

C:\JSR175\examples>javac -cp logannotation.jar;. AnnotationSampleCli
ent.java

C:\JSR175\examples>java AnnotationSampleClient
Calls foo(calls foo, 10)
Calls bar(1.0)
Calls bar()
 
C:\JSR175\examples>

別段何ということもありません。

さて、ここで次に示すツールを使用してみましょう。

Doclet の JAR ファイル loglet.jar

そして、この JAR ファイルを使用して JavaDoc を実行します。環境変数 JAVA_HOME は J2SDK をインストールしたディレクトリを示しています。

C:\JSR175\examples>javadoc -classpath "%JAVA_HOME%\lib\tools.jar";lo
glet.jar;logannotation.jar -doclet Loglet AnnotationSample.java
ソースファイル AnnotationSample.java を読み込んでいます...
Javadoc 情報を構築しています...
Proxy Name: AnnotationSampleProxy.java
Factory Name: AnnotationSampleFactory.java
  
C:\JSR175\examples>

すると、AnnotationSampleProxy.java と AnnotationSampleFactory.java というファイルが生成されました。

先ほどの AnnotationSampleClient クラスを生成されたクラスを使用して書き換えましょう。AnnotationSampleFactory クラスには AnnotationSample インスタンスを生成する create というメソッドが定義されているので、これを使用してオブジェクトを生成するようにしました。

public class AnnotationSampleClient2 {
    public AnnotationSampleClient2() throws Exception {
//        AnnotationSample sample = new AnnotationSample();
//        sample = new AnnotationSample("test", true);

        AnnotationSample sample = AnnotationSampleFactory.create();
        sample = AnnotationSampleFactory.create("test", true);
 
        sample.foo("calls foo", 10);
        sample.bar(1.0);
        sample.bar();
    }
 
    public static void main(String[] args) throws Exception {
        new AnnotationSampleClient2();
    }
}

コンパイルして、実行です。

あ、忘れていました。実行する前にちょっと細工がいります。%JAVA_HOME%\jre\lib\logging.properties ファイルを 2 箇所修正しておきます。

#.level= INFO      # 修正前
.level= ALL        # 修正後
 
#java.util.logging.ConsoleHandler.level = INFO   # 修正前
java.util.logging.ConsoleHandler.level = ALL     # 修正後

今度こそ準備ができたので、実行です。

C:\JSR175\examples>java AnnotationSampleClient2
2004/03/12 8:42:51 AnnotationSampleProxy <init>
設定: Default Constructor
2004/03/12 8:42:51 AnnotationSampleProxy <init>
設定:  title: test flag: true
2004/03/12 8:42:51 AnnotationSampleProxy foo
設定:  msg: calls foo x: 10
Calls foo(calls foo, 10)
2004/03/12 8:42:51 AnnotationSampleProxy foo
設定: return
2004/03/12 8:42:51 AnnotationSampleProxy bar
設定:  x: 1.0
Calls bar(1.0)
2004/03/12 8:42:51 AnnotationSampleProxy bar
設定:  return: 1
Calls bar()
  
C:\JSR175\examples>

AnnotationSample クラスには一切手を入れていないのに、先ほどとはずいぶん違う出力がされました。この出力は Logging API を使用したログになります。この例ではコンソールにログを出力しましたが、もちろんファイルに出力することもできます。

どうしてこんなことができるのでしょうか。

それは Metadata を使用したからです。

JavaDoc を実行したときに 2 つのファイルが生成されましたが、それを生成しているのが LogLet という Doclet です。Doclet というのは JavaDoc をカスタマイズするために使用されるもので、通常 API マニュアルを生成するのも Doclet で実装されています。

Doclet には JavaDoc からクラスの詳細情報が渡されます。その中にはアノテーションの情報も含まれています。LogLet はそれを見て @Log と書かれているメソッドはログを出力するような AnnotationSample クラスのプロキシクラスを生成します。また、プロキシがすぐに使えるようなファクトリを生成しているのです。

生成されたプロキシクラスはつぎのようになっています (見やすいように整形をしています)。

public class AnnotationSampleProxy extends AnnotationSample{
    private static java.util.logging.Logger logger
                   = java.util.logging.Logger.getLogger("AnnotationSample");
 
    public AnnotationSampleProxy()  {
        super();
        logger.log(java.util.logging.Level.CONFIG, "Default Constructor");
    }
 
    public AnnotationSampleProxy(java.lang.String title, boolean flag)
                                     throws java.lang.IllegalArgumentException {
        super(title, flag);
        logger.log(java.util.logging.Level.CONFIG,
                   ""  + " title: " + title + " flag: " + flag);
    }
	
    public void foo(java.lang.String msg, int x) throws java.lang.Exception {
        logger.log(java.util.logging.Level.CONFIG, "msg: " + msg + " x: " + x);
        super.foo(msg, x);
        logger.log(java.util.logging.Level.CONFIG, "return");
    }
	
    public int bar(double x)  {
        logger.log(java.util.logging.Level.CONFIG, "x: " + x);
        int result = super.bar(x);
        logger.log(java.util.logging.Level.CONFIG, "return: " + result);
        return result;
    }
}

AnnotationSample#bar() メソッドは @Log がついていないので、このプロキシクラスには表れていません。その他のメソッドだけがログ出力されるわけです。

このようなクラスを生成できるのも Metadata が使えるからなのです。

 

 
 
Tiger Annotation はどのように記述するか
 
 

アノテーションを記述することができるのはクラス、インタフェース、アノテーション、メソッド、フィールド、そしてなんとパッケージにも記述することができます。いずれの場合もアノテーションはモディファイアと同列に扱われます。ようするに public や static, final などと同列だということです。

ですから、アノテーションをクラスやメソッドなどに付記するときには public などと一緒に記述することになります。

たとえば、クラスにアノテーションをつけるには、次の 1. と 2. の方法のどちらでも OK ということです。

1.
  @SomeClassAnnotation
  public class SomeClass {
      ...
 

2.
  public @SomeClassAnnotation class SomeClass {
      ...

また、アノテーションもパッケージがあるので、適時 import 文で指定する必要があります。

アノテーション は次の 3 種類に分けることができます。

  • 普通のアノテーション
  • マーカ アノテーション (Marker Annotation)
  • シングルメンバ アノテーション (Single Member Annotation)

これらの違いは属性 (アノテーションの言葉はメンバとなっているので、以後はメンバと表記します) があるかないかということです。

たとえば、先の LogLet で使用した @Log というアノテーションは value というメンバを持っています。このメンバはログのレベルを表すものです。次のように記述した場合はログのレベルが SEVER であるということを示しています。

    @Log(value=LogLevel.SEVER)
    public String foo() {
        ...

先ほどの 3 種類は、マーカ アノテーションがメンバを持たないもの、シングルメンバ アノテーションが value というメンバだけを持つもの、それ以外という区分になります。

Clonable や Serializable などメソッドを定義していないインタフェースをマーカ インタフェースといいますが、マーカ アノテーションもそれと同様です。アノテーションがあるという情報だけを付記できるわけです。

シングルメンバ アノテーションはメンバが value だけなので、次に示したように省略した書き方をすることができます。

    @Log(value=LogLevel.SEVER)
    public String foo() {
        ...
=
    @Log(LogLevel.SEVER)
    public String foo() {
        ...

メンバで使用できる型には

  • プリミティブ型
  • 文字列
  • Class クラス
  • enum 型
  • アノテーション
  • 上記の型の配列

@Log アノテーションでは LogLevel という enum 型を定義して使用しています。配列の場合は {} を使用し、カンマ区切りで記述します。

    @Authors(authors={"櫻庭", "桜庭", "Sakuraba"})
    public String foo() {
        ...

もちろん、複数の属性を持っているアノテーションもあります。

    @Authors(lastname="櫻庭", firstname="祐一")
    public String foo() {
        ...

シングルメンバ アノテーションの value メンバが配列の場合、配列の要素が 1 つであれば次のような記述も可能です。

    @Authors(value={"櫻庭"})
    public String foo() {
        ...
=
    @Authors("櫻庭")
    public String foo() {
        ...

アノテーションがどのような型のどのようなメンバを持つかは JavaDoc に記載されています。たとえば、java.lang.annotation.Target の JavaDoc は次のようになっています。


java.lang.annotation
Annotation Type Target


@Documented @Retention(value=RUNTIME) 
                  @Target(value=ANNOTATION_TYPE)  public @interface Target 
              

省略


Required Member Summary
 ElementType[] value

 

Member Detail

value

public abstract ElementType[] value

これを見ると enum の java.lang.annotation.ElementType 型の配列である value という属性があることがわかります。したがって、このアノテーションはシングルメンバ アノテーションだということです。

ところで、この Target というアノテーションは自分自身を自分で注釈しています。なんかへんな感じですが、こういうことも可能だということです。

Annotation を使うのはそれほど難しくないのですが、Annotation がどのような意図を持って定義されているかを理解する必要があります。どのようなアノテーションがあって、どのような時に使用するかを覚えなくては、有効にアノテーションを使えないというわけです。

 

 
 
Tiger Annotation を定義してみよう
 
 

普通のプログラミングでは Metadata を使うことはあっても、それを定義したり、Metadata を扱うツールなどを作ることはめったにないと思います。

しかし、あえてアノテーションを定義してみましょう。定義自体はとても簡単です。

アノテーションは一種のインタフェースとして定義されます。interface と記述するところを @interface と書きます。ただし、通常のインタフェースに比べると以下のような制約があります。

  • 派生はできない
  • Generics を使用することはできない

これを守れば、定義自体はインタフェースとほとんど同じです。もっとも単純なマーカ アノテーションだと次のようになります。

public @interface Marked {}

アノテーションを書くときにメンバの値を指定できましたが、メンバはなんとメソッドとして定義します。たとえば、@Log は次のようになっています。

package jp.gr.java_conf.skrb.annotation;
 
public @interface Log {
    LogLevel value();
}

@Log のメンバは value でしたが、それがそのままメソッドになってしまいます。

メソッドとして定義はしますが、いろいろと制約の多いメソッドになります。

  • 引数はなし
  • Generics は使用できない
  • 戻り値は次の型だけを使用できる
    • プリミティブ型
    • 文字列
    • Class クラス
    • enum 型
    • アノテーション
    • 上記の型の配列
  • throws は記述できない

@Log の場合は戻り値が LogLevel 型ですが、これは enum なので使用できるわけです。ところで、この戻り値の型はどこかで見たことありませんか? そうです、メンバとして使用できる型と一緒です。

要するに、メンバに値を代入することは、このメソッドをコールしたときに代入された値が戻るようになっているのです。値を読み出すのはもうちょっと後で試してみましょう。

戻り値にアノテーションが使えるからといって、自分をメソッドの戻り値にすることはできません。ようするに次の例はだめということです。

public @interface ItsMe {
    ItsMe value();    // NG
}

これに関連して循環するような定義もできません。

public @interface Ah {
    Un value();    // NG
}
 
public @interface Un {
    Ah value();    // NG
}

もう 1 つ、通常のメソッドと違うのがデフォルト値を設定できるということです。デフォルト値はメソッドの定義の後に default と記述し、その後に記述します。

たとえば、@Log は実をいうとデフォルト値が設定されていました。

package jp.gr.java_conf.skrb.annotation;
 
public @interface Log {
    LogLevel value() default LogLevel.CONFIG;
}

いままではメソッドは 1 つでしたが、もちろん複数書くことも可能です。

public @interface Service {
    String description();
    String endpoint();
    String protocol() default "SOAP";
}

定義も簡単でしょ ;-)

最後に @Log アノテーションの全体を示しておきます。

package jp.gr.java_conf.skrb.annotation;
 
public @interface Log {
    LogLevel value() default LogLevel.CONFIG;
}

value メソッドで使用する LogLevel クラス (というか enum 型ですが) は次のようになっています。

package jp.gr.java_conf.skrb.annotation;
 
public enum LogLevel {
    SEVERE,
    WARNING,
    INFO,
    CONFIG,
    FINE,
    FINER,
    FINEST
}

 

 

 
 
Tiger Annotation を読み込む (Reflection 編)
 
 

アノテーションの定義までできるようになりましたが、定義してもそれを読み込めるようにならなければ利用することはできません。

アノテーションの読み込みには 2 つの方法があります。

  1. 実行時に読み込む
  2. オフラインで読み込む

Reflection を使用する場合、アノテーションが記述されたクラスをロードし、リフレクションを利用してアノテーションを読み込みます。

オフラインで読み込むときでもリフレクションを使用してもいいのですが、そのためにはクラスをロードしなくてはいけません。クラスをロードせずに読み込む方法として LogLet でも使用した JavaDoc を利用する方法と apt というツールが提供されています。apt は別のドキュメントに解説しました。

まずはリフレクションを使った方法から試してみましょう。

リフレクションでアノテーションを扱うために AnnotatedElement というインタフェースが導入されています。AnnotatedElement インタフェースで定義しているメソッドは次の 4 種類です。

  • <T extends Annotatoin> T getAnnotation(Class<T> annotationType)
  • Annotation[] getAnnotations()
  • Annotation[] getDeclaredAnnotaions()
  • boolean isAnnotationPresent(Class<? extends Annotations> annotationsType)

AnnotatedElement インタフェースをインプリメントしているクラスには

  • java.lang.Class
  • java.lang.reflect.Field
  • java.lang.reflect.Constructor
  • java.lang.reflect.Method
  • java.lang.reflect.Package

です。Class クラスだけはこれ以外に isAnnotation というメソッドがあって、Class クラスが指しているクラスがアノテーションのクラスかどうかを判別することができます。

それでは手始めにどのようなアノテーションが記述されているか調べて見るプログラムを作ってみましょう。

サンプルのソース AnnotationsReader.java

調べる順序として次のようにしたいと思います。

  1. 調べるクラスがアノテーションかどうか
  2. クラスに付記されているアノテーション
  3. フィールドに付記されているアノテーション
  4. コンストラクタに付記されているアノテーション
  5. メソッドに付記されているアノテーション

まずはクラスがアノテーションかどうかです。これには前述した Class#isAnnotation メソッドを使用します。

    public void readAnnotation(String classname) {
        try {
            Class cls = Class.forName(classname);
            System.out.print(cls.getName() + " is ");
            if (cls.isAnnotation()) {
                System.out.println("Annotation.\n");
            } else {
                System.out.println("not Annotation.\n");
            }
            
            System.out.print(cls.getName() + " is ");
            readAnnotations(cls);
 
            readAnnotation(cls.getDeclaredFields(), "Field");
            readAnnotation(cls.getDeclaredConstructors(), "Constructor");
            readAnnotation(cls.getDeclaredMethods(), "Method");
        } catch (ClassNotFoundException ex) {
            System.err.println(classname + " が見つかりません");
        }
    }

調べるクラスの Class クラスがないと話にならないので、まず Class#forName メソッドを使用して Class オブジェクトを取得します。

次に Class#isAnnotation メソッドを使用してアノテーションかどうかを調べています。

次の行からがクラス、フィールド、コンストラクタ、メソッドと順に調べていくためのコードになっています。readAnnotation メソッドは次のようになっています。

    public void readAnnotation(AnnotatedElement[] elements, String message) {
        for (AnnotatedElement element : elements) {
            System.out.print(message + "[" + element +"] is ");
            readAnnotations(element);
        }
    }

フィールドもコンストラクタもメソッドも AnnotatedElement インタフェースをインプリメントしているので、こんな書き方をすることができます。

そして、拡張 for 文を使用して要素を切り出し、readAnnotations メソッドをコールしています。肝心の readAnnotations メソッドは次のようになります。

    private void readAnnotations(AnnotatedElement element) {
        Annotation[] annotations = element.getAnnotations();
 
        if (annotations.length != 0) {
            System.out.print("Annotated.\nAnnotation:\n");
            for (Annotation annotation : annotations) {
                System.out.println("    " + annotation);
            }
            System.out.println();
        } else {
            System.out.println("not Annotated.\n");
        }
    }

まず、AnnotatedElement#getAnnotaions メソッドを使用して記述されているすべてのアノテーションを取得します。アノテーションが記述されていない場合、AnnotatedElement#getAnnotations メソッドの戻り値は空の配列が戻ってくるので、それを調べるためのメソッドが次の if 文になっています。

アノテーションがあればそれを 1 つずつ切り出し、出力しています。

それではコンパイル、実行してみます。実行するときには引数としてクラスを指定します。まずは標準で提供されている java.lang.annotation.Target クラスを読み出してみましょう。

C:\JSR175\examples>javac AnnotationsReader

C:\JSR175\examples>java AnnotationsReader java.lang.annotation.Target
java.lang.annotation.Target is Annotation.

java.lang.annotation.Target is Annotated.
Annotation:
    @java.lang.annotation.Retention(value=RUNTIME)
    @java.lang.annotation.Target(value=[ANNOTATION_TYPE])

Method[public abstract java.lang.annotation.ElementType[] java.lang.annotation.T
arget.value()] is not Annotated.
  
C:\JSR175\examples>

Target クラスはアノテーションで、クラスには Retention アノテーションと Target アノテーションが付記されています。また、value メソッドはアノテーションがないことが出力から分かります。

次にサンプルとして作った AnnotationSample クラスを読み込ませてみます。

C:\JSR175\examples>java AnnotationsReader AnnotationSample
AnnotationSample is not Annotation.

AnnotationSample is not Annotated.

Constructor[public AnnotationSample(java.lang.String,boolean) throws java.lang.I
llegalArgumentException] is not Annotated.

Constructor[public AnnotationSample()] is not Annotated.

Method[public void AnnotationSample.foo(java.lang.String,int) throws java.lang.E
xception] is not Annotated.

Method[public void AnnotationSample.foo() throws java.lang.Exception] is not Ann
otated.

Method[public int AnnotationSample.bar(double)] is not Annotated.

Method[public int AnnotationSample.bar()] is not Annotated.
  
C:\JSR175\examples>

あれっ ? ちゃんとアノテーションが出力されていません。これはどういうことなのでしょうか。

実をいうとこれでかなり悩みました。どうして出力されないのか ?

答えはクラスファイルにありました。

アノテーションを導入するために、当然ながらクラスファイルのフォーマットが変更されています。クラスファイルのヘッダ部分にアノテーションの情報が埋め込まれるようになっています。単にアノテーションの情報だけかと思っていたのですが、どうやら違うようです。

AnnotationSample クラスのクラスファイルをダンプしてみると、次のようになりました。

Address   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F       ASCII
---------------------------------------------------------------------------
00000000  CA FE BA BE 00 00 00 31 00 53 0A 00 11 00 32 09  .......1.S....2.
00000010  00 33 00 34 07 00 35 0A 00 03 00 32 08 00 36 0A  .3.4..5....2..6.
00000020  00 03 00 37 08 00 38 0A 00 03 00 39 08 00 3A 0A  ...7..8....9..:.
00000030  00 03 00 3B 0A 00 3C 00 3D 08 00 3E 08 00 3F 0A  ...;..<.=..>..?.
00000040  00 03 00 40 08 00 41 07 00 42 07 00 43 01 00 06  ...@..A..B..C...
00000050  3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43  ...()V...C
00000060  6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72  ode...LineNumber
00000070  54 61 62 6C 65 01 00 1B 52 75 6E 74 69 6D 65 49  Table..RuntimeI
00000080  6E 76 69 73 69 62 6C 65 41 6E 6E 6F 74 61 74 69  nvisibleAnnotati
00000090  6F 6E 73 07 00 44 01 00 05 76 61 6C 75 65 01 00  ons..D...value..
000000A0  28 6A 70 2E 67 72 2E 6A 61 76 61 5F 63 6F 6E 66  (jp.gr.java_conf
000000B0  2E 73 6B 72 62 2E 61 6E 6E 6F 74 61 74 69 6F 6E  .skrb.annotation
000000C0  2E 4C 6F 67 4C 65 76 65 6C 01 00 04 49 4E 46 4F  .LogLevel...INFO
000000D0  01 00 16 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53  ..(Ljava/lang/S
000000E0  74 72 69 6E 67 3B 5A 29 56 01 00 12 4C 6F 63 61  tring;Z)V...Loca
000000F0  6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00  lVariableTable..
               ... 以下省略 ...

注目していただきたいのは、赤い字のところです。RuntimeInvisibleAnnotations と書かれています。同じように Target クラスをダンプしてみると、

Address   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F       ASCII
---------------------------------------------------------------------------
00000000  CA FE BA BE 00 00 00 31 00 16 07 00 11 07 00 12  .......1.......
00000010  07 00 13 01 00 05 76 61 6C 75 65 01 00 25 28 29  ......value..%()
00000020  5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 61 6E 6E 6F  [Ljava/lang/anno
00000030  74 61 74 69 6F 6E 2F 45 6C 65 6D 65 6E 74 54 79  tation/ElementTy
00000040  70 65 3B 01 00 0A 53 6F 75 72 63 65 46 69 6C 65  pe;...SourceFile
00000050  01 00 0B 54 61 72 67 65 74 2E 6A 61 76 61 01 00  ...Target.java..
00000060  19 52 75 6E 74 69 6D 65 56 69 73 69 62 6C 65 41  RuntimeVisibleA
00000070  6E 6E 6F 74 61 74 69 6F 6E 73 07 00 14 01 00 24  nnotations....$
00000080  6A 61 76 61 2E 6C 61 6E 67 2E 61 6E 6E 6F 74 61  java.lang.annota
00000090  74 69 6F 6E 2E 52 65 74 65 6E 74 69 6F 6E 50 6F  tion.RetentionPo
000000A0  6C 69 63 79 01 00 07 52 55 4E 54 49 4D 45 07 00  licy...RUNTIME..
000000B0  11 01 00 20 6A 61 76 61 2E 6C 61 6E 67 2E 61 6E  ... java.lang.an
000000C0  6E 6F 74 61 74 69 6F 6E 2E 45 6C 65 6D 65 6E 74  notation.Element
000000D0  54 79 70 65 01 00 0F 41 4E 4E 4F 54 41 54 49 4F  Type...ANNOTATIO
000000E0  4E 5F 54 59 50 45 01 00 1B 52 75 6E 74 69 6D 65  N_TYPE..Runtime
000000F0  49 6E 76 69 73 69 62 6C 65 41 6E 6E 6F 74 61 74  InvisibleAnnotat
               ... 以下省略 ...

こちらは RuntimeVisibleAnnotations と記述されています。

どうやらこの違いがリフレクションでアノテーションを読めるかどうかの違いのようです。そこで、Target アノテーションと Log アノテーションを比較してみました。主だった違いは次の 2 つです。

  1. Documented アノテーションが使用されている
  2. Retention アノテーションが使用されている
  3. Target アノテーションが使用されている

Target アノテーションは次のように定義されています (コメントは省略しています)。

package java.lang.annotation;
 
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

標準で用意されているこれらのアノテーションは後で説明するとして、一番怪しいのは Retention アノテーションです。value メンバには enum の RetentionPolicy 型を指定できるのですが、RetentionPolicy 型が定義しているのは

  • CLASS
  • RUNTIME
  • SOURCE

の 3 つです。それぞれの JavaDoc の説明を見てみると、アノテーションがクラスファイルに含むようにするには CLASS か RUNTIME を使用するように書かれています。SOURCE の場合はクラスファイルにアノテーションの情報は記述されないようです。

CLASS と RUNTIME の違いは、実行時にアノテーションを読めるかどうかで、RUNTIME の場合のみ読み込めると説明されています。

そこで、Log アノテーションにこれを付加してみました。

package jp.gr.java_conf.skrb.annotation;
 
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    LogLevel value() default LogLevel.CONFIG;
}

さて、結果は...

C:\JSR175\examples>java AnnotationsReader AnnotationSample
AnnotationSample is not Annotation.

AnnotationSample is not Annotated.

Constructor[public AnnotationSample(java.lang.String,boolean) throws java.lang.I
llegalArgumentException] is Annotated.
Annotation:
    @jp.gr.java_conf.skrb.annotation.Log(value=CONFIG)

Constructor[public AnnotationSample()] is Annotated.
Annotation:
    @jp.gr.java_conf.skrb.annotation.Log(value=INFO)

Method[public void AnnotationSample.foo(java.lang.String,int) throws java.lang.E
xception] is Annotated.
Annotation:
    @jp.gr.java_conf.skrb.annotation.Log(value=FINE)

Method[public void AnnotationSample.foo() throws java.lang.Exception] is Annotat
ed.
Annotation:
    @jp.gr.java_conf.skrb.annotation.Log(value=CONFIG)

Method[public int AnnotationSample.bar(double)] is Annotated.
Annotation:
    @jp.gr.java_conf.skrb.annotation.Log(value=CONFIG)

Method[public int AnnotationSample.bar()] is not Annotated.
  
C:\JSR175\examples>

見事読み出すことができました。また、AnnotationSample.class をダンプしても RuntimeVisibleAnnotations と記述されているのが確認できます。

ところでこの RuntimeVisibleAnnotations とか RuntimeInvisibleAnnotations というのは何なんでしょう。何のことはなく、メタデータの仕様書に書いてありました。って、はじめから仕様書読んでればこんな苦労はしなかったのに ...

クラスファイルにはアノテーションのアトリビュートとして次の 5 種類のアトリビュートが定義されています。

  1. RuntimeVisibleAnnotations
  2. RuntimeInvisibleAnnotations
  3. RuntimeVisibleParameterAnnotations
  4. RuntimeInvisibleParameterAnnotations
  5. AnnotationDefault

これらのアトリビュートはそれぞれ情報を保持しているのですがそこまで見る必要はなさそうです。問題はこららのアトリビュートが何を表しているかということです。

RuntimeVisibleAnnotations は今まで見てきたように実行時にいつでもアノテーションを参照することができるためのアトリビュートです。

逆に RuntimeInvisibleAnnotations は実行時にリフレクションでアノテーションを参照することはできません。しかし、アノテーションの情報はクラスファイルに記述されるので、リフレクション以外の別の機構を使えば読み出すことは可能なはずです。

RuntimeVisibleParameterAnnotations と RuntimeInvisibleParameterAnnotations は RuntimeVisibleAnnotations/RuntimeInvisibleAnnotations とほとんど同じですが、メソッド定義の中だけに記述されるものらしいです。

最後の AnnotationDefault はアノテーションのメンバのデフォルト値に関するアトリビュートを示しているようです。

これらを明確に意識する必要はないと思いますが、Retention アノテーションによってこれらのアトリビュートが変化して、リフレクションで読めるかどうかを決めるということは意識したほうがいいと思います。

次にもう少しアノテーションを詳しく見てみるサンプルを作ってみました。Log アノテーションを読み出すサンプルです。

サンプルのソース LogAnnotationReader.java

アノテーションを取得するには AnnotatedElement インタフェースの getAnnotation(Class<T> annotationType) メソッドを使用します。戻り値はパラメータ化されているので、直接引数で指定したアノテーション型に代入することが可能です。

基本的なところは先ほどの AnnotationsReader クラスと同じなので、実際に getAnnotation メソッドを使用している部分を次に示します。

    private void readAnnotation(AnnotatedElement element) {
        Log log = element.getAnnotation(Log.class);
 
        if (log != null) {
            LogLevel level = log.value();
            System.out.println("annotated.\n    value: " + level);
        } else {
            System.out.println("not annotated.\n");
        }
    }

getAnnotations メソッドを使用することで Log オブジェクトを取得することができました。Log アノテーションが記述されていない場合、戻り値は null になります。

Log オブジェクトが取得できたので、メンバの値を取得することができます。アノテーションの定義でメンバはメソッドとして定義すると書きましたが、ここでそれを利用するわけです。

Log アノテーションのメンバ value は value() メソッドに対応しています。value() メソッドをコールすると、アノテーションで記述された値を取得することができるのです。

このサンプルを実行してみると、次のようになりました。

C:\JSR175\examples>java AnnotationsReader AnnotationSample
Checking class AnnotationSample ...

Constructor[public AnnotationSample(java.lang.String,boolean) throws java.lang.I
llegalArgumentException] is annotated.
    value: CONFIG
Constructor[public AnnotationSample()] is annotated.
    value: INFO
Method[public void AnnotationSample.foo(java.lang.String,int) throws java.lang.E
xception] is annotated.
    value: FINE
Method[public void AnnotationSample.foo() throws java.lang.Exception] is annotat
ed.
    value: CONFIG
Method[public int AnnotationSample.bar(double)] is annotated.
    value: CONFIG
Method[public int AnnotationSample.bar()] is not annotated.
  
C:\JSR175\examples>

メンバの値も正しく読み込まれているのがわかると思います。

 

 
 
Tiger Annotation を読み込む (Doclet 編)
 
 

これまでは実行時にアノテーションを読み出すことを試してみましたが、それ以外の方法でもアノテーションを読み出すことが可能です。

そのために提供されているのが Doclet API です。Doclet は JavaDoc から起動されます。通常は API ドキュメントを記述するためですが、JavaDoc から得ることができるクラスやメソッドの情報を使って他の用途に使用することもできます。

また、Doclet を使うと、リフレクションでアノテーションを読み出すのとは異なり、クラスファイルが必要ありません。つまり、クラスローダリングしなくてもアノテーションを読み込むことが可能です。

それでは、一番はじめに使用した LogLet を材料に、Docllet でのアノテーションの扱いについて解説していきましょう。

サンプルのソース Loglet.java

Doclet は Applet や Servlet と違い、クラスではありません。Doclet を使用するには static な start メソッドを定義するだけです。

    public static boolean start(RootDoc root){ 
        try {
            new Loglet(root);
        } catch (IOException ex) {
            ex.printStackTrace();
            return false;
        }
 
        return true;
    }

引数の RootDoc オブジェクトにクラスやメソッド、アノテーションの情報が保持されています。戻り値は boolean で解析が成功すれば true、失敗したら false を返すようにします。

RootDoc クラスのようなクラス JavaDoc のページDoclet API で JavaDoc を読むことができます。Doclet についての詳細は省略するとして、アノテーション関連の Doclet のクラスは

  • AnnotationTypeDoc
  • AnnotationTypeMemberDoc
  • AnnotationDesc
  • AnnotationDesc.MemberValuePair
  • AnnotationValue

の 5 つです。

AnnotationTypeDoc クラスはアノテーション型自体を表しているクラスです。AnnotationTypeMemberDoc クラスがそのアノテーションのメンバの情報を保持しています。

残りの 3 つがソース中に記述されたアノテーションの情報を表すためのクラスです。AnnotationTypeDoc クラスが記述されたアノテーションを表し、そのメンバの情報が AnnotationTypeMemberDoc に保持されます。実際のメンバの値は AnnotationValue クラスで表されます。

実際に記述されたアノテーションを取得するには ProgramElementDoc インタフェースの annotations メソッドを使用します。annotations メソッドの戻り値は AnnotationDesc クラスの配列なので、後はそこから必要な情報を引き出していきます。

たとえば、Log アノテーションがあるかどうかをチェックする部分は次のようになりました。

    private boolean checkAnnotation(ProgramElementDoc[] elements) {
        for (int i = 0; i < elements.length; i++) {
            AnnotationDesc[] annotations = elements[i].annotations();
            for (int j = 0; j < annotations.length; j++) {
                AnnotationTypeDoc type = annotations[j].annotationType();
                if (type.name().equals(LOG_ANNOTATION)) {
                    return true;
                }
            }
        }
 
        return false;
    }

引数の members は ProgramElementDoc インタフェースの配列ですが、実際にはそれをインプリメントしたコンストラクタ (ConstructorDoc クラス) もしくはメソッド (MethodDoc クラス) の情報が入っています。

3 行目で annotations メソッドを使用して AnnotationsDesc クラスの配列を取り出し、そこから AnnotationTypeDoc オブジェクトを取得します。そして、その名前が "Log" であるかどうかをチェックしています。

Doclet ではこのようにアノテーションを扱います。

せっかくなので、作成した Loglet の解説もしましょう。

Loglet クラスはアノテーションを利用してログを書き出すプロキシとそのプロキシを生成するためのファクトリクラスを自動生成しています。全体的な流れは以下に示すアクティビティ図は図 1 のようになります。アクティビティ図は UML で定義されている図の中の 1 つで、フローチャートのような使い方をします。

全体のフロー メソッド単位のフロー
図 1 全体のアクティビティ図 図 2 メソッド・コンストラクタ単位のアクティビティ図

図 2 はプロキシやファクトリを生成するときに、それぞれのメソッド・コンストラクタを作成するときのフローになります。この流れに基づいて説明をしていきたいと思います。

図 1 ではループになっていませんが、RootDoc クラスには複数のクラスの情報が入っているので、すべてのクラスについて図 1 のフローを行います。

    public Loglet(RootDoc root) throws IOException {
        ClassDoc[] classes = root.classes();
 
        for (int i = 0; i < classes.length; i++) {
            if (checkAnnotation(classes[i])) {
                createProxy(classes[i]);
                createFactory(classes[i]);
            }
        }
    }

まずはプロキシ生成から見ていきます。プロキシはクラス定義などの部分、コンストラクタ、メソッド、クラスの最後の部分という順番で作成していきます。次に示したようにプロキシのファイル名はオリジナルのクラス名 + Proxy.java というようにしています。

    private void createProxy(ClassDoc cls) throws IOException {
        String proxyName = cls.name() + "Proxy.java";
        System.out.println("Proxy Name: " + proxyName);
 
        PrintWriter writer = new PrintWriter(
                                 new BufferedWriter(
                                     new FileWriter(proxyName)));
        writeHeader(cls, writer);
        writeConstructors(cls, writer);
        writeMethods(cls, writer);
        writeFooter(writer);

        writer.close();
    }

writeHeader メソッドや writeFooter メソッドはアノテーションには関係ないので、省略します。writeConstructor メソッドと witeMethod メソッドが図 2 のフローで処理を行っています。それぞれ似かよった処理になるので、ここでは writeMethod メソッドだけを見ていきましょう。

writeMethod メソッドはかなり長いので行番号を振ってみました。

01:    public void writeMethod(MethodDoc method, PrintWriter writer) {
02:        if (checkModifier(method)) {
03:            return;
04:        }
05: 
06:        AnnotationDesc[] annotations = method.annotations();
07:        if (annotations.length == 0) {
08:            return;
09:        }
10: 
11:        boolean logFlag = false;
12:        String level = DEFAULT_LOG_LEVEL;
13:        for (int j = 0; j < annotations.length; j++) {
14:            AnnotationTypeDoc type = annotations[j].annotationType();
15:            if (type.name().equals(LOG_ANNOTATION)) {
16:                logFlag = true;
17:
18:                String tmpLevel = extractLogLevel(annotations[j]);
19:                if (tmpLevel != null) {
20:                    level = tmpLevel;
21:                    break;
22:                }
23:            }
24:        }
25:
26:        if (!logFlag) {
27:            return;
28:        }
29: 
30:        Parameter[] params = method.parameters();
31:        
32:        writer.print(method.modifiers() + " " 
33:                     + method.returnType().qualifiedTypeName() + " " 
34:                     + method.name() + "(");
35:        writeParameterDefs(params, writer);
36:        writer.print(") ");
37:        
38:        writeExceptions(method, writer);
39:        writer.println(" {");
40:        
41:        writePreLog(params, level, "calls " + method.name(), writer);
42:        
43:        if (!method.returnType().qualifiedTypeName().equals("void")) {
44:            writer.print("    " + method.returnType().qualifiedTypeName()
45:                         + " result = ");
46:        } else {
47:            writer.print("    ");
48:        }
49:        
50:        writer.print("super." + method.name() + "(");
51:        writeParameters(params, writer);
52:        writer.println(");");
53:        
54:        writePostLog(method, level, writer);
55:        
56:        writer.println("}");
57:    }

まずは図 2 にあるとおり、public もしくは protected であるかのチェックです (2 行目から 4 行目)。public/protected であったとしても、final なメソッドの場合はオーバライドしないようにしています。

6 行目でこのメソッドに付加されているアノテーションを取得します。アノテーションがない場合、annotations メソッドは空の配列を返すので、7, 8 行目でそれをチェックし、アノテーションがなければ writeMethod メソッドを抜けます。

11 行目からが Log アノテーションの内容を調べている部分です。annotations メソッドで取り出した AnnotationDesc オブジェクトから、そのアノテーションの型情報を保持している AnnotationTypeDoc オブジェクトを取得します (14 行目)。

それが Log であるかどうかを調べているのが 15 行目です。名前を得るために com.sun.javadoc.Doc インタフェースで定義されている name メソッドを使用します。name メソッドはパッケージを含まない名前を返すので、それと "Log" を比較しています。

アノテーションが Log であれば、value メンバの値を取得します。18 行目の extractLogLevel メソッドは次のようになっています。

    private String extractLogLevel(AnnotationDesc annotation) {
        AnnotationDesc.ElementValuePair[] members = annotation.elementValues();
        for (int i = 0; i < members.length; i++) {                
            if (members[i].element().name().equals(LOG_VALUE)) {
                return members[i].value().toString();
            }
        }
 
        return null;
    }

メンバの情報を取得するには AnnotationDesc クラスの elementValues メソッドを使用します。AnnotationDesc.ElementValuePair クラスはメンバの型情報を持つ AnnotationTypeMemberDoc オブジェクトとメンバの値を保持する AnnotationValue オブジェクトを保持しています。

メンバの値をとるためには value メソッドを使用します。Log の場合はそれがログのレベルとなっています。

気をつけなくてはいけないのが、AnnotationDesc#elementValues メソッドはメンバが明記されているものだけを返します。デフォルト値が設定されているものは、このメソッドでは得られる配列には含まれていません。

デフォルト値は AnnotationTypeMemberDoc クラスが保持しています。

ログのレベルが分かったので、後はプロキシーのコードを出力するだけです。基本的には次のようなコードを出力することになります。

    private 戻り値の型 メソッド(引数) throws 例外 {
        ログ出力のためのコード
 
        戻り値の型 result = super.メソッド(引数);
 
        ログ出力のコード
 
        return result;
    }

戻り値がない場合はじゃっかん異なりますが、だいたい同じと考えていいと思います。たとえば、メソッドのシグネチャを書く部分は 30 行目から 36 行目になります。

まず、parameters メソッドで引数の情報を取得します。そして、記述するメソッドのモディファイア (modifiers メソッド)、戻り値 (returnType メソッド)、メソッド名 (name メソッド) をまず記述します。

戻り値の型を出力しているところで name メソッドでなく qualifiedTypeName メソッドを使用しているのは、パッケージからフルに書かせるためです。パッケージを書かないと import 文を記述しなくてはいけないのですが、それよりは qualifiedTypeName メソッドを使用したほうが単純にすみます。

メソッドの引数の並びは writeParameterDefs メソッドで記述しています。

    private void writeParameterDefs(Parameter[] params, PrintWriter writer) {
        if (params.length > 0) {
            for (int i = 0; i < params.length - 1; i++) {
                writer.print(params[i].type().qualifiedTypeName()
                             + " " + params[i].name() +", ");
            }
            writer.print(params[params.length - 1].type().qualifiedTypeName()
                         + " " + params[params.length - 1].name());
        }
    }

ループが param.length - 1 までになっているのは、最後の引数だけは "," (カンマ) が書かれないためです。

後は同じようにして、ログを書き、親クラスを呼び出し、ログを書き、return を書いて、最後のカッコを書くという流れになります。

このようにしてアノテーションを使うことで、本来プログラムには関係ない情報を付加することができます。そして、その情報を使うことでファイルの自動生成などができるのです。

 

 
 
Tiger 標準で用意されている Annotation
 
 

Tiger では標準でいくつかのアノテーションが定義されています。前述した Target アノテーションや Retention アノテーションも標準で定義されているアノテーションです。これらの使い方を調べてみましょう。

java.lang.annotation.Target

Target アノテーションはアノテーションを修飾するためのアノテーションです。ドラフトにはアノテーションを細くするためのアノテーションなのでメタアノテーションと記述されています。

Target はアノテーションの使用法について説明を加えるものです。すなわち、あるアノテーションがクラスに付記されるものなのか、メソッドに付記されるものなのか、それともフィールドに付記されるものなのかという情報を記述します。

Target アノテーションの value メンバは ElementType の配列となります。ElementType は次のように定義されています。

public enum ElementType {
    /** Class, interface or enum declaration */
    TYPE,
 
    /** Field declaration (inlcudes enum constants) */
    FIELD,
 
    /** Method declaration */
    METHOD,
 
    /** Parameter declaration */
    PARAMETER,
 
    /** Constructor declaration */
    CONSTRUCTOR,
 
    /** Local variable declaration */
    LOCAL_VARIABLE,
 
    /** Annotation type declaration */
    ANNOTATION_TYPE,
 
    /** Package declaration */
    PACKAGE
}

たとえば、A というアノテーションがクラスに付記される場合、次のように定義します。

@Traget({ElementType.CLASS})
public @interface A {}

また、Log アノテーションであればコンストラクタとメソッドに対応しているので、次のように書くことができます。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface Log {
    LogLevel value() default LogLevel.CONFIG;
}

Target アノテーションの value メンバは配列なので、こういう複数の要素に使用されるアノテーションも定義することができます。

Target アノテーションは javac が使用しています。たとえば、Log アノテーションを次のようにクラスに付加してみます。

import jp.gr.java_conf.skrb.annotation.Log;

@Log
public class WrongAnnotationUse { public void foo() {} }

これをコンパイルすると

C:\JSR175\examples>javac WrongAnnotationUse
WrongAnnotationUse.java:3: annotation type not applicable to this kind of declar
ation
@Log
 ^
エラー 1 個
  
C:\JSR175\examples>

というように怒られてしまいます。

 

java.lang.annotation.Retention

Retention アノテーションは前述したようにアノテーションの用途を説明するためのメタアノテーションです。メンバは value だけで型は RetentionPolicy です。先に示したように RetentionPolicy は 3 つの定数を定義しています。

定数 意味
CLASS クラスファイルにアノテーションの情報が記述されるが、リフレクションでは読み込めない
RUNTIME クラスファイルにアノテーションの情報が記述され、実行時にリフレクションを用いて読み出すことが可能
SOURCE クラスファイルにはアノテーションの情報が記述されない。

このアノテーションも Target アノテーションと同様に javac か解釈します。

 

java.lang.annotation.Documented

Documented アノテーションはメンバを持たないマーカアノテーションです。やはり、アノテーションを修飾するためのメタアノテーションとなります。

Documented アノテーションはアノテーションを JavaDoc などで扱うかどうかを示しています。

たとえば、Log アノテーションは Documented アノテーションを使用していないので、AnnotationSample クラスの JavaDoc を作成するとつぎのようになります (部分)。

Constructor Detail

AnnotationSample

public AnnotationSample()

AnnotationSample

public AnnotationSample(java.lang.String title,
                        boolean flag)
                 throws java.lang.IllegalArgumentException
Throws:
java.lang.IllegalArgumentException

Log アノテーションに Documented アノテーションを付加すると次のように変化しました。

Constructor Detail

AnnotationSample

@Log(value=INFO)
public AnnotationSample()

AnnotationSample

@Log
public AnnotationSample(java.lang.String title,
                            boolean flag)
                 throws java.lang.IllegalArgumentException
Throws:
java.lang.IllegalArgumentException

コンストラクタの説明のところに @Log と記述されているのが分かると思います。

このように Documented アノテーションは JavaDoc が解釈して、API ドキュメントに情報を反映させるものなのです。

 

java.lang.annotation.Inherited

Inherited アノテーションもメンバを持たないメタ マーカアノテーションです。

このアノテーションが付記されたアノテーションを使用すると、使用されたクラスの派生クラスにもそのアノテーションが引き継がれます。たとえば、次のようなアノテーションを考えて見ます。

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
 
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InheritedAnnotation {}

これを使用するクラスとして

@InheritedAnnotation
public class InheritedAnnotationSample {}
 
public class InheritedAnnotationSample2 extends InheritedAnnotationSample {}

これで前に作った AnnotationsReader クラスでアノテーションを読み取ってみましょう。

C:\JSR175\examples>java AnnotationsReader InheritedAnnotationSample
InheritedAnnotationSample is not Annotation Type.

InheritedAnnotationSample is Annotated.
Annotation:
    @InheritedAnnotation()

Constructor[public InheritedAnnotationSample()] is not Annotated.
  
C:\JSR175\examples>java AnnotationsReader InheritedAnnotationSample2
InheritedAnnotationSample2 is not Annotation Type.

InheritedAnnotationSample2 is not Annotated.

Constructor[public InheritedAnnotationSample2()] is not Annotated.

Constructor[public InheritedAnnotationSample()] is not Annotated.C:\JSR175\examples>

これだと InheritedAnnotationsSample2 クラスにはアノテーションはついていません。

そこで InheritedAnnotation アノテーションに次のように Inherited アノテーションをつけてみます。

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Target;
 
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InheritedAnnotation {}

同様に AnnotationsReader を実行してみると

C:\JSR175\examples>java AnnotationsReader InheritedAnnotationSample2
InheritedAnnotationSample2 is not Annotation Type.

InheritedAnnotationSample2 is Annotated.
Annotation:
    @InheritedAnnotation()

Constructor[public InheritedAnnotationSample2()] is not Annotated.

C:\JSR175\examples>

InheritedAnnotationSample2 クラスは変更していないのに、InheritedAnnotation アノテーションが付記されているという結果になりました。

このように Inherited アノテーションを使用することで、アノテーションの影響する範囲を自身のクラスだけにするか派生クラスを含めたものにするかのコントロールをすることができます。

 

java.lang.Override

java.lang パッケージの 2 つのアノテーションは java.lang.annotation パッケージで定義されたアノテーションと異なり、メタアノテーションではありません。両方とも普通に使用されるアノテーションで、Override はメソッドに付記することができます。

Override アノテーションは派生クラスが親クラスのメソッドをオーバライドしたときにつけるようにします。Override アノテーションがあると、javac は親クラスのメソッドを参照して正しくオーバライドしているかチェックします。

たとえば、つぎのように親クラスのメソッドを 1 つはオーバライド、1 つはオーバロードしたとします。両方に Override アノテーションをつけておくと、javac がエラーを出力します。

public class OverridesAnnotationSample {
    public void foo() {
        System.out.println("foo!");
    }
}
 
  
public class OverridesAnnotationSample2 extends OverridesAnnotationSample {
    @Override
    public void foo() {
        System.out.println("bar!");
    }
 
    @Override
    public void foo(String message) {
        System.out.println("bar: " + message);
    }
}

 

C:\JSR175\examples>javac OverridesAnnotationSample2
OverridesAnnotationSample2.java:7: method does not override a method from its su
perclass
    @Override
     ^
エラー 1 個

C:\JSR175\examples>

もとのソースの 7 行目はオーバロードしたほうなので、正しくオーバライドしていないということを javac が検出します。

JavaOne で恒例となっている Joshua Bloch と Neal Gafter の Programming Puzzler でこんな問題が出されたことがあります。下のプログラムを実行すると出力はどうなるかという問題です。

public class Name {
    private Strint first, last;
 
    public Name(String first, String last) {
        if (first == null || last == null)
            throw new NullPointerException();
 
        this.first = first;
        this.last = last;
    }
 
    public boolean equals(Name o) {
        return first.equals(o.first) && last.equals(o.last);
    }
  
    public int hashCode() {
        return 31 * first.hashCode() + last.hashCode();
    }
 
    public static void main(String[] args) {
        Set s = new HashSet();
        s.add(new Name("Mickey", "Mouse");
        System.out.println(s.contains(new Name("Mickey", "Mouse")));
    }
}

答えは 3 択です。

  1. true
  2. false
  3. It varies

さて、答えはどれでしょうか。

答えは 2. の false です。

この問題の肝は Object#equals の引数が Object クラスであるということにあります。オーバライドしたつもりでも、オーバロードになってしまったんですね。

こんな間違いを減らすために Overrides アノテーションは威力を発揮しそうです。

 

java.lang.Deprecated

Deprecated アノテーションはメソッドなどが Deprecated であることを示すために使用されます。これまでは Depricated であることを示すには JavaDoc の @deprecated タグを使用していましたが、Deprecated アノテーションの方が簡潔に表すことができます。

たとえば、次のような例を見てみましょう。

public class DeprecatedAnnotationSample {
    @Deprecated
    public void foo() {
        System.out.println("foo");
    }
 
    @Deprecated
    public void bar() {
        System.out.println("bar");
    }
}


public class DeprecatedAnnotationSample2 extends DeprecatedAnnotationSample {
    public void foo() {
        System.out.println("foo 2");
    }
 
    public void bar2() {
        bar();
    }
}

これをコンパイルすると

C:\JSR175\examples>javac DeprecatedAnnotationSample2
注: DeprecatedAnnotationSample2.java は推奨されない API を使用またはオーバーライ
ドしています。
注: 詳細については、-Xlint:deprecation オプションを指定して再コンパイルしてくだ
さい。

C:\JSR175\examples>

そこで、-Xlint:deprecation を使ってコンパイルしなおすと次のように出力されます。

C:\JSR175\examples>javac -Xlint:deprecationDeprecatedAnnotationSamp
le2
DeprecatedAnnotationSample2.java:2: 警告: DeprecatedAnnotationSample の foo() は
推奨されません。
    public void foo() {
                ^
DeprecatedAnnotationSample2.java:7: 警告: DeprecatedAnnotationSample の bar() は
推奨されません。
        bar();
        ^
警告 2 個
 
C:\JSR175\examples>

このようにメソッドを使用したりオーバライドすることを警告してくれます。

 

 
 
Tiger どんな仕組みになっているのか
 
 

虎の穴では恒例の裏側がどうなっているか調べて見ましょう。

まずはアノテーションの定義についてです。アノテーションは java.lang.annotation.Annotation インタフェースの派生インタフェースになるとドラフトに書いてあるのでそのとおりなのでしょう。

いつものように Jad を使って Log アノテーションを逆コンパイルしてみました。

package jp.gr.java_conf.skrb.annotation;

import java.lang.annotation.*;

public interface Log
    extends Annotation
{
    public abstract LogLevel value();
}

やはり Annotation インタフェースの派生クラスになっています。しかし、Log アノテーションに付記していた Target などのアノテーションの情報はなくなってしまっています。

これはまだ jad が Tiger のクラスファイルに対応していないのでしかたがないでしょう。jad 1.5.6 でサポートしているクラスファイルのバージョンは 45.3, 46.0, 47.0 で、Tiger のクラスファイルのバージョンは 49.0 になります。

jad が Tiger のクラスファイルに対応したらもう少し詳しく見てみることにして、今はこのぐらいにしておきましょう。

ドラフトによれば、先に示したような RuntimeVisibleAnnotations などのアトリビュートを使用してアノテーションの情報が記述され、method_info 構造体の中に含まれるようになると書いてあります。

とりあえず、Annotation インタフェースがどのようなインタフェースかぐらいは見ておきましょう。Annotation インタフェースで定義されているメソッドは 2 つです。

  • boolean equlas(Object obj)
  • String toString()

これだけだとなんか拍子抜けしてしまいますね ^^;;

メンバが配列の場合、メンバの要素すべてを比較しないと equlas メソッドが実装できません。そこで、java.util.Arrays クラスに hashCode(int[] a) などのプリミティブ型に対するメソッド群が用意されました。また、文字列を生成するときも、配列の要素すべてを記述しなくてはならないので、同様に Arrays クラスに toString(int[] a) などのメソッド群が用意されています。

オブジェクトの配列の場合は Arrays#deepHashCode(Object[] a)、Arrays#deepEquals(Object[] a1, Object[] a2)、Arrays#deepToString() が提供されます。

 

 
 
Tiger おわりに
 
 

この解説ではアノテーションを自分で定義して、それに対応したツールを作るところまで行いましたが、普通はそんなことしないとは思います。

しかし、だからといって Annotation が役に立たないわけではありません。

さまざまなツールやライブラリがアノテーションに対応したらこれほど便利なものはありません。重要なのはどのようなアノテーションがあって、それがどのような使われ方をするかを覚えなくてはいけないということです。

大変なような気がするかもしれませんが、普通にライブラリのクラスを覚えて使うのとたいした違いはありません。ただ、その使われ方が今までのクラスとはちょっと違うというだけです。

きたるべき J2EE 1.5 に備えて、今から使い方だけでも慣れておくといいのではないでしょうか。

 

今回使用したサンプルはここからダウンロードできます。

参考

(Mar. 2004)
(改訂 Aug. 2005)

 
 
Go to Contents Go to Java Page