LoginSignup
12
11

More than 5 years have passed since last update.

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

Posted at

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

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

12
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
11