0. はじめに
Java で毎回毎回 null チェックするの、何とかならないかなぁと思っていたら、Eclipse に Null Analysis という機能があることに気付いた。(IntelliJ の方が先に実装してるっぽいけど)
色々調べたので、Null Analysys の使い方(2018年10月時点)をまとめておく。
ちなみに使っている Eclipse バージョンは 2018-09 (4.9.0)。基本的に Juno (4.2) 以降は Null Analysis が出来るが、いろいろバグがあったりするので最新版の方が良い。
1. Null Analysis とは
「いちいち null チェックしたくないよね」を Javaらしくアノテーションを付けて静的解決する手段。詳しくは、 Eclipse Junoでnull解析を行う 参照。
なお、ちゃんと Null Analysis の恩恵を受けるには、Java 8 以降が必要。
Java 8 で実装された JSR 308 の型アノテーションが無いと、あまり恩恵が受けられない。
2. どのアノテーションを使うべきか
アノテーションには、Eclipse のもの、IntelliJ IDEA のもの等、似て非なるものが色々あって選択が難しい。
自分の場合、あまり特定 IDE の色を入れたくないので、選択肢としては、
- JSR 305 の
@javax.annotation.Nonnull
系 - Checker Framework の
@org.checkerframework.checker.nullness.qual.NonNull
系
のどちらかになる。各種アノテーションについては、Nonnullって、何ですか? が詳しい。
さらにJava 9 以降のモジュールシステムを考慮すると JSR 305 の@javax.annotation.Nonnull
は使えない。簡単に言うと、javax.annotation.*
が Java 標準モジュールとして入っているところに 外部モジュールの javax.annotation.Nonnull
を使おうとすると、パッケージ名前空間が衝突してしまうため。詳しくは Making JSR 305 Work On Java 9 を参照。
というわけで使うアノテーションは Checker Framework のアノテーション一択になる。
が、Eclipse 上で使うにはひとつ問題がある。
Eclipse では Null Analysis 対象とするパッケージを @NonNullByDefault
アノテーションで判定するのだけど、Checker Framework はそもそもデフォルトが全部 NonNull 扱いなので、それっぽいアノテーションを持っていない(@DefaultQualifier
はなんか違う。。引数必要だし)。
ここは少し妥協して @NonNullByDefault
は Eclipse のアノテーションを使うようにしよう。後で切り替えるにしても、それほど苦労しないはず。
3. Null Analysis の準備
アノーテションを使う時は Maven Central から取ってきましょう。build.gradle
例は以下の通り。
// https://mvnrepository.com/artifact/org.checkerframework/checker-qual
implementation group: 'org.checkerframework', name: 'checker-qual', version: '2.5.6'
// https://mvnrepository.com/artifact/org.eclipse.jdt/org.eclipse.jdt.annotation
implementation group: 'org.eclipse.jdt', name: 'org.eclipse.jdt.annotation', version: '2.2.100'
Eclipse 標準では Null Analysis はオフなので、コンパイラ設定も必要。やり方は、Nonnullって、何ですか? の下の方に書いてある。
4.コードの書き方
4-1. 自作パッケージに @NonNullByDefault
を付ける
基本的に自作パッケージは全部デフォルト NonNull にする。
各自作パッケージの package-info.java
にアノテーションを付けるだけ。
@org.eclipse.jdt.annotation.NonNullByDefault
package common.validator;
こうすると、以下のようなコードが
public class Foo {
String a;
public String bar(String b) {
String bb = b + b;
return bb;
}
}
以下のように、全体的に @NonNull
が付いている感じになる。
public class Foo {
@NonNull String a; // フィールドは @NonNull
@NonNull // 返り値も @NonNull
public String bar(@NonNull String b) { // 仮引数も @NonNull
@NonNull String bb = bb + bb; // ローカル変数も @NonNull
return bb;
}
}
4-2. @NonNull
と @Nullable
@NonNull
が付いている箇所には null が入らないことがコンパイル時(Eclipseだとコード記述時リアルタイム)に保証される。
ちなみに、null を入れたい箇所には @Nullable
を付ける。
@NonNull
が付いている変数に null を入れようとするとコンパイルエラーになるし、
@NonNull String a = null; //コンパイルエラー
null じゃなくても、@Nullable
な変数を@NonNull
な変数に代入しようとすると、コンパイルエラーになる。
@Nullable String a = "a";
@NonNull String s = a; //コンパイルエラー
メソッド呼び出しも同様。上記例クラスのメソッドに @Nullable
な変数を入れようとするとコンパイルエラーになる。
Foo foo = new Foo();
@Nullable String nullableStr = "a";
String s = foo.bar(nullableStr); //ここでコンパイルエラー
なお、当然ながら、@Nullable
な変数に @NonNull
な変数を代入することは可能。
@NonNull String nonNullStr = "a";
@Nullable String nullableStr = nonNullStr; //OK
4-3. @Nullable
変数を @NonNull
にするには
外部とのやり取りでDTOパターンを使ったりすると、どうしても @Nullable
な値が入ってくるので、何らかの手段で @NonNull
に変換する必要がある。
外部ライブラリなども @NonNull
を付けたりしないので、戻り値が @Nullable
扱いになったりするし。
変換は @NonNull
な変数に代入することで行うが、普通に代入するとコンパイルエラーになるので、コンパイラに @Nullable
だけど null じゃないよ、ということを伝える必要がある。
伝え方1 : Java 7 導入の Objects で伝える
Objects#requireNonNull()
を通すと、コンパイラが検査済みと判断してくれる。null が来ないことが自明な場合はこの伝え方が簡単。もし null が来ると NullPointerException
になる。
@Nullable String nullableStr = 〜〜;
Objects.requireNonNull(nullableStr); // Null のときは NullPointerException
@NonNull String nonNullStr = nullableStr; //OK
Null Analysis を使うと、ちょくちょくこのコードを使うので、Java Editor Templateに入れておくと便利。
自分の場合は、以下の定義を入れている。 reqn
と書いて Ctrl+Space
で、良い感じにコードが入る。
Name : ReqNotNull
Context : Java Statement
Automatically insert : true
Pattern:
${:import (java.util.Objects)}Objects.requireNonNull(${localVar})
伝え方2 : if文で伝える
null のときに NullPointerException
を出したくない場合はこの方法。
@Nullable String nullableStr = 〜〜;
if (nullableStr != null) { // if 文で伝える
@NonNull String nonNullStr = nullableStr; //OK
}
その他
他にも伝え方があれば教えて欲しい。
4-4. 配列/Listのようなコンテナ型について
配列の場合
配列の場合は少し考慮が必要。(@NonNullByDefault
パッケージ配下で)普通にフィールド宣言すると、配列変数自体は @NonNull
になるが、配列の中に入るオブジェクトは @Nullable
になる。
public class Foo {
String[] args; // args は NonNull だが、args[0] は Nullable
}
中身も @NonNull
にしたい場合は、アノテーションを付ける。
public class Foo {
@NonNull String[] args; // args も args[0] も NonNull
}
可変長引数を定義するときも同じ。
public class Foo {
// アノテーションなし
public void bar(String...args) {
// args は NonNull だが、args[0] は Nullable
}
// アノテーションあり
public void baz(@NonNull String...args) {
// args も args[0] も NonNull
}
}
List の場合
List の場合は、配列と違ってコンテナも中身も @NonNull
になる。
public class Foo {
List<String> args; // args も args.get(0) も NonNull
}
5. 不明点
よく分かっていない点のメモ
-
JSR 305 アノテーションを使っていたときは
Object#toString()
等の標準メソッドが Nullable で面倒だったのが、Checker Framework + Eclipse アノテーションにすると、NonNull 扱いになっていた。何故だ? -
JSR 305 アノテーションを使っていたときはデフォルト NonNull なのはメソッドの仮引数だけだったが、Checker Framework + Eclipse アノテーションにするとフィールドも戻り値も NonNull になっていた。Eclipse がアノテーション名を見て動作を変えている?
6. おわりに
何か知見が増えたら書き足す予定。