Java (Eclipse) で Null 安全なプログラム


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. おわりに

何か知見が増えたら書き足す予定。