KAnnotatorでJavaライブラリでもNull-Safety機能を使う #Kotlin

Kotlin Advent Calendar 2013 22日目のエントリです。

Javaのライブラリ使いたい

Kotlin使ってて普通にサードパーティ製のライブラリ使いますよね。例えばJoda-Time使ってこんな感じで。

fun main(args: Array<String>) {
    val today = LocalDate.now()
    val first = today!!.dayOfMonth()!!.withMinimumValue()
    println(first!!.toString("mm/dd"))
}

すごい勢いある感!!

fun main(args: Array<String>) {
    val today = LocalDate.now()
    val first = today?.dayOfMonth()?.withMinimumValue()
    println(first?.toString("mm/dd"))
}

すごい疑問ある感?

便利な機能であるはずのKotlinのNull-Safety機能ですが、Javaライブラリでメソッドチェーンしたいときなんかは、どうしても微妙な感じになります。そのメソッド絶対null返さないから!って言いたくなる、なんかめっちゃnull不安な人のコードみたい。空気読んで欲しいですよね。

External Annotations

これを解決する手段として、JetBrainsが用意している型アノテーションがあります。Java側のコードで@NotNull*1、@Nullable*2が付いていると、KotlinではこれをNull-Safety機能として解釈してくれます。*3

ただ、だからと言ってJoda-Timeとか既存ライブラリいじって@NotNull付けて使うかと言われれば、そんなめんどくさいこと毎回やってたら大変だよね……ってところで登場するのが「External Annotations」です。

External Annotationsは、ライブラリを改変せずにXMLで「このメソッドに対してこのアノテーションを付ける」みたいな感じでアノテーションを外部定義できます。IntelliJ IDEAの機能の1つです。*4

Joda-Timeのnowメソッドまで移動して、おもむろにAlt+Enterを押します。

なんかそれっぽいの出ました。「Annotate method 'now' as @NotNull」を選択します。たぶん初回はExternal AnnotationsのXMLを保存する場所を聞かれるので、適当に選びましょう。

選択したディレクトリの中にパッケージ構成分のディレクトリが出来ています。そこにannotations.xmlというファイルがあって、中身はこんな感じになっているはず。

<root>
    <item name='org.joda.time.LocalDate org.joda.time.LocalDate now()'>
        <annotation name='org.jetbrains.annotations.NotNull'/>
    </item>
</root>

元のコードに戻ると、警告出てますね。

これで、Joda-Timeには何一つ手を加えることなく、メソッドに@NotNullアノテーションを付けて、Kotlin側からもnowメソッドの戻り値はNull-Safe型で扱えるようになりました。

でもこれdayOfMonth、withMinimumValue、さらに他のメソッドも全部同じことするかって言われれば、そんなめんどくさいことやってられないよね。

KAnnotator

External Annotations便利なんですが、それを一つ一つやるのはさすがに狂気の沙汰です。しかも@NotNull、@Nullableのアノテーションを選択するのは自分なので、ミスる可能性もあります。そこで「KAnnotator」の登場です。

KAnnotatorは 任意のライブラリに含まれる全メソッドの中身を推論し、External Annotationsを生成する IntelliJ IDEAのプラグインです。GitHubで管理されていて、IntelliJ IDEAのプラグインリポジトリからインストールできます。

新規プラグインインストールで検索ボックスに"Kotlin"と入力する*5とKAnnotatorが出現するので、インストールします。

ちなみにこれ、エントリ書いてる時点ででKotlinのDowndowd数が26000ぐらいに対してKAnnotatorが1650件ぐらいです。あんまり知られてない?

インストールが終わって再起動したら、メニューのAnalyzeに「Annotate Jar Files...」って項目が追加されてるので選択します。

ちょっと加工してますがこんな感じのウィンドウが出てくるので、Joda-Timeのライブラリにチェックを入れて、OK押します。そんなに待たずにExternal Annotationsの生成が完了します。

生成したXMLを見てみると、大量のメソッドに対して@NotNullのExternal Annotations定義が書かれています。@NullableのExternal Annotationsはわざわざ定義していないようです。*6

再度コードを見てみると、他の安全呼び出しにも警告が出ています。

というわけで、これでKotlinは空気を読めるようなりました!やったー!

KAnnotator / External Annotations のすごいところ

  • JavaライブラリでNull-Safety機能が簡単に使える!!!
  • ライブラリ側の対応を待たずに、@NotNull, @Nullableアノテーションが使える
  • アノテーションが大量でコードがぐちゃぐちゃ!みたいな感じにならない
  • ライブラリ側でExternal Annotationsを提供すればKotlin側で何の違和感もなく使える
  • Kotlin+External AnnotationsでJava側からは型アノテーションついてるように見える

今後Java8が正式リリースされて、Kotlinではなくとも型アノテーションを活用するシーンはいろいろ増えてくると思うので、そういう場合も簡単に対応できそうなのがいいですね。JetBrainsの公式でも「Annotate The World」なんて言ってますし。

現状KAnnotatorはNullabilityアノテーションしか対応していないみたいですが、今後は@Mutable, @ReadOnlyなどの他アノテーションへの対応や、@NonNullと@NotNullの違いの吸収とか、そのへんも対応してほしい!

Javaの標準ライブラリのExternal Annotations

このエントリを書くためにIntelliJ IDEAセットアップしてて今更気づいたんですけど*7、標準ライブラリってnullを返さないメソッドはNull-Safe型が返るようになってたんですね。

たとえばこんなコードとか。

fun main(args: Array<String>) {
    val cal = Calendar.getInstance();
    cal.set(Calendar.DATE, cal.getActualMinimum(Calendar.DATE))
    println("${cal.get(Calendar.MONTH) + 1}/${cal.get(Calendar.DATE)}")
}

変数calから呼び出してるメソッドの前には?がいるはず。これもExternal Annotationsでやってました。

Project Structure開いて→Platform SettingsのSDKs→Annotationsタブを開くと、Intellij IDEA側で持ってるJDKのExternal Annotationsと、KotlinのExternal Annotationsがインポートされてます。

ちなみにこれ、Files.get(String, String...)が@NotNullじゃないっぽいんですけど、あれってnull返す可能性あるんだっけかな……?

まとめ

KotlinでJavaライブラリ使うならKAnnotator使わない理由ない気がしてきた!試してないけどAndroidSDKのライブラリも対応できそう。

今回試したコードGitHubにあげたのでURL貼っておきます https://github.com/clomie/KotlinKAnnotatorTest

個人的には、コードがよりシンプルに書けるはずのKotlinで、言語機能の売りであるNull-Safety機能がJavaライブラリで微妙な感じになってしまうのがKotlinの残念な部分だと思っていたので、それが解決できた今Kotlin最強なんじゃないかと思ってきました。

Kotlin最強!Kotlinかわいい!\ホノカチャーン/

あとこんなに長いエントリ書いたの久々なのでちょっと疲れました。

参考URL

Using External Annotations | Project Kotlin

Kotlin M4 is Out! | Project Kotlin

*1:org.jetbrains.annotations.NotNull

*2:org.jetbrains.annotations.Nullable

*3:以前こういう機能あればいいのにみたいな話をしてたんですが、実はM3あたりから出来るようになってたらしい

*4:たぶん

*5:KAnnotatorでもOK

*6:たぶん……

*7:だからこそこのエントリ書いてる