JSR 308対応のコンパイラとそのプラグイン

前回は、11月9日に公開されたJSR 308:Annotations on Java TypesのEarly Draftを元に、同仕様の概要を紹介した。JSR 308については本連載第15回でも取り上げているが、その際、開発段階にある対応コンパイラを用いて型に対するアノテーションの適用例を紹介した。

そのJSR 308対応コンパイラだが、現在の最新版はバージョン0.3.99となっており、Compiler plug-ins(Type-checking Plug-ins)で使用できるアノテーションも追加されている。今回はそれを用いて、@NonNull以外のアノテーションを使ってみようと思う。

コンパイラおよびCompiler plug-insはこのページで公開されている。まずコンパイラはファイル「jsr308-langtools.zip」をダウンロードする。これを展開するとlibtoolsディレクトリ (以下、これを[LIBTOOLS_DIR]と表記) が納められている。ビルドするにはまずmakeディレクトリにあるbuild.propertiesの32行目付近にあるブートストラップJDKの設定を、リスト1のように自身の環境にインストールされているJDKのものに変更する。

リスト1 build.propertiesを編集

boot.java.home = D:/Program Files/Java/jdk1.6.0_03
boot.java = ${boot.java.home}/bin/java
boot.javac = ${boot.java.home}/bin/javac
boot.javac.target = 6

その上でmakeディレクトリに移動し、Apache Antを用いてプロンプト1のようにビルドを実行する。

プロンプト1 コンパイラのビルド

> cd [LANGTOOLS_DIR]/make
> ant build-javac

すると[LIBTOOLS_DIR]/dist/libディレクトリにjavac.jarファイルが、[LIBTOOLS_DIR]/dist/binディレクトリにjavacコマンドが生成される。このjavacコマンドはjavac.jarを用いてコンパイルを実行するためのシェルスクリプトである。したがってシェルが使える環境ならば、[LIBTOOLS_DIR]/dist/binを環境変数PATHの最初 (インストール済みのJDKより前) に追加しておけば、javacコマンド実行時にJSR 308対応のコンパイラが呼び出されることになる。Windows環境では-jarオプションでjavac.jarを実行する (後述)。

Compiler plug-insのビルド方法は第15回で紹介したものと同様だ。「jsr308-checkers.zip」をダウンロードして任意の場所に展開し、中に含まれるcheckersディレクトリ(以下、これを[CHECKERS_DIR]と表記)に移動してApache Antを用いてビルドする(プロンプト2)。

プロンプト2 Compiler plug-insのビルド

> cd [CHECKERS_DIR]
> ant

生成されたchekers.jarに各種アノテーションが含まれているので、これをクラスパスに含めてコンパイルすればよい。

さて、Compiler plug-insには現在以下のアノテーションが用意されている。

  • @NonNull - nullを許容しない
  • @Interned - オブジェクトどうしの"=="または"!="での比較をチェック
  • Javariで提供されるMutabilityに関するアノテーション
  • IGJで提供されるMutabilityに関するアノテーション

Javari: Java with reference immutabilityでは、Mutability(変更可能性)に関する@ReadOnly、@Assignable、@Mutable、@ReadOnly、@RoMaybeなどのアノテーションが提供されている。また、IGJではJavariと同等の機能を持つ@ReadOnly, @Mutable, @Immutable, @Assignable, @AssignsFields, @Iなどが提供される。

@Internedアノテーションを試す

@Internedアノテーションは、オブジェクトどうしの"=="演算子や"!="演算子による比較を検証する機能を持つ。これらの演算子はオブジェクトの参照が同一かどうかを比較するものだが、非常に初歩的な間違いとして、これをオブジェクトどうしの値の比較に使っているケースがある。値を比較する際は本来ならばequals()メソッドを使用しなければならず、この間違いがバグの温床になってしまう。

Compiler plug-insでは、オブジェクトどうしを"=="や"!="で比較すると警告を発するようになっている(現状では例外が発生する)。そして、@Internedアノテーションを付加した型オブジェクト同士を比較する場合のみ、この警告を回避することができる。

たとえばリスト2のようなプログラムの場合、internedStr1/internedStr2/internedStr3はそれぞれ比較可能だが、normalStr1およびnormalStr2は@Internedではないため、演算子による比較ができない。

リスト2 InternedSample.java - @Internedアノテーションの使用例

import checkers.quals.Interned;

class InternedSample {
    public static void main(String[] args) {
        @Interned String internedStr1 = "foofoo";
        @Interned String internedStr2 = "foofoo";
        @Interned String internedStr3 = new @Interned String("foofoo");
        String normalStr1 = "hogehoge";
        String normalStr2 = new String("hogehoge");

        System.out.println(internedStr1 == internedStr2);   // true
        System.out.println(internedStr2 == internedStr3);   // false
        // System.out.println(normalStr1 == normalStr2);    // エラー
        // System.out.println(internedStr1 == normalStr1);  // エラー
        // System.out.println(internedStr3 != normalStr2);  // エラー
    }
}

コンパイル方法は、-typeprocessorオプションでcheckers.interned.InternedCheckerを指定してプロンプト3のようにする。Windowsのコマンドプロンプトの場合はシェルが使えないので、プロンプト4のように-jarオプションでコンパイラを実行する。

プロンプト3 @Internedアノテーションを使う場合のコンパイル

> javac -classpath [CHEKERS_JAR_PATH] -typeprocessor checkers.interned.InternedChecker InternedSample.java

プロンプト4 Windows環境でのコンパイル方法

> java -jar [LANGTOOLS_DIR]\dist\lib\javac.jar -classpath [CHEKERS_JAR_PATH] -typeprocessor checkers.interned.InternedChecker InternedSample.java

Javariの@ReadOnlyアノテーションを試す

JavariはJavaオブジェクトに対して変更可能/不可能を明示的に指定できるようにするためのライブラリである。Compiler plug-insには、Javariによる変更可能性を指定するためのアノテーションが統合された。たとえば@ReadOnlyアノテーションを指定した型のオブジェクトは、読み込み専用とされて変更することができなくなる。

リスト3では、@ReadOnlyを用いて変更不可能なStringBufferオブジェクトを作っている。この場合、roBuffは@ReadOnlyなのでappend()メソッドを用いると警告される。また、@ReadOnlyなオブジェクトを通常の(Mutableな)型の変数に代入することはできない。

リスト3 ReadOnlySample.java - Javariの@ReadOnlyアノテーションの使用例

import checkers.quals.ReadOnly;

class ReadOnlySample {
    public static void main(String[] args) {
        StringBuffer nBuff = new StringBuffer("foofoo");
        @ReadOnly StringBuffer roBuff = new StringBuffer("hogehoge");

        nBuff.append("barbar");        // OK
        //roBuff.append("piyopiyo");   // エラー

        //nBuff = roBuff;              // エラー

        roBuff = nBuff;                // OK
        //roBuff.append("piyopiyo");   // エラー
    }
}

Javariによるアノテーションを使用する場合、コンパイル時の-typeprocessorオプションの引数にはプロンプト5のように「checkers.javari.JavariChecker」を指定する。

プロンプト5 Javariによるアノテーションを使う場合のコンパイル

> javac -classpath [CHEKERS_JAR_PATH] -typeprocessor checkers.javari.JavariChecker ReadOnlySample.java

JSR 308を利用すればJavaの型に対して明示的な制限を設けることができ、それをコンパイル時に静的に検証することが可能になる。今回は用意されたアノテーションを利用したが、独自のアノテーションを作成することももちろん可能だ。JSR 308はJava SE 7に標準APIとして導入される予定となっている。