2016/02/06

Android: NonNullアノテーション

Android Studio 0.5.5から@NonNullアノテーションがサポートされた.
今回はメソッドの引数に@NonNullアノテーションをつけるケースについて書いた.

@NonNull

@NonNullアノテーションはフィールドやメソッドの引数, メソッドのReturn値にNullを許容しないことを表明するアノテーションである.
このアノテーションが便利な点は, @NonNullアノテーションにNullを代入したり, 戻り値のNullチェックをしたりする場合に, IDEが@NonNullの制約をCode Inspectionで示してくれるところだ.

ただし, これはIDE付属のCode Inspection設定で報告レベルを調整可能でもあるし, @NonNullの値にNullを設定したからといってシンタックスエラーにはならない.
@NonNullはドキュメンテーションや支援機能であって, Nullを防ぐものではない点に注意が必要である.

下記のコードはhogeメソッドの引数sにNullを渡す恐れがある. しかし, これはCode Inspectionとして報告されない.

private String piyo;

public void foo() {
    hoge(piyo);
}

public void hoge(@NonNull String s) {
}

@NonNullはメソッドの呼び出し元でNullが渡らないことを保証する必要がある.
Code Inspectionは@NonNullフィールド, メソッド引数, 戻り値の非Null性を保証するものではないからだ.

@NonNullの使いどころ

メソッドが引数にNullを許容せず, NullPointerExceptionをスローするケースは少なくない.
そういう時こそ@NonNullアノテーションの出番だ. メソッドの利用者に配慮する意味でも@NonNullを是非つけておきたいところだ.

@NonNullをつければ, メソッド引数の事前条件確認としてのNullチェックはスキップしてもいいだろう.
ただ, “もしNullが渡ってきたら”を考えておいたほうが良さそうなケースがある.

例えば, 次のコードの場合を考えてみる.

public class Hoge {
    @NonNull private final Foo foo;

    public Hoge(@NonNull Foo foo) {
        this.foo = foo;
    }

    public String bar() {
        this.foo.call();
    }

    public void piyo(@NonNull Foo f) {
        f.call();
    }
// ...

クラスHogeはコンストラクタで@NonNullなfooを引数にとる.
このコンストラクタがNullPointerExceptionを返すことはないが, Hogeクラスとしてメンバ変数fooがnullの状態になることを許容したくないのだ.
それを明示するためにもコンストラクタの引数fooには@NonNullアノテーションを付けておいた.

もし, HogeのコンストラクタでfooにNullが渡った場合のことを考えると少し厄介な問題を引き起こすかもしれない.

まず, メソッドbarを確認してみる.
this.fooの値は同クラスのコンストラクタで@NonNullな引数fooで初期化されたものであり, 非Nullであることが事前条件として成立している(おまけにfinal付きだ).
もし, 仮にthis.fooがNullであるような事態に陥ったとしてもbarは実行時の予期せぬ例外としてNullPointerExceptionをスローする.
try-catchで例外を握りつぶすこともない. このメソッドのコードに問題はなさそうだ.

次に, コンストラクタを確認してみる.
このコンストラクタはシンプルで, 引数fooを受け取り, これでメンバー変数を初期化する.
もし, 仮にメソッドの引数fooにNullが代入されてしまっても, このコンストラクタは何の異常も報告しない(にも関わらず@NonNullアノテーションを使用している).
このケースは問題に発展するかもしれない.

メンバ変数fooが意図せずNullで初期化されていることを知るのはメソッドbarを実行した時だ.
クラスHogeが単純な責務しかもたず, 特定のクラスとしか関連しない場合や, コンストラクタで初期化した直後にbarを呼ぶ場合は, fooにNullを渡した犯人を突き止めることができるかもしれない.
しかし, クラスHogeが複数のクラスで共有されるshared objectのような立場であった場合.
Nullでthis.fooが初期化されるタイミングでは沈黙し, Nullを渡した犯人とは異なる運の悪い別インスタンスがbarを呼ぶことではじめてNullPointerExceptionが報告される.

こうなると話はややこしくなる.
当然, NullPointerExceptionのスタックトレースに犯人の痕跡は残っていない.
@NonNullであるはずの引数fooに一体誰がNullを代入したのか. NonNull Code Inspectionの網をくぐり抜けた犯人を追うことになる.

事件はすぐに解決できるかもしれないが, 事件を未然に防ぐことに努めるべきだろう.
クラスHogeのコンストラクタで何かできることはあるだろうか.

まず@NonNullのアノテーションを外してみよう. そしてJavadocの事前条件欄にでも“Nullは受け付けない”旨を強調した上で, コードでもNullチェックをしてみよう.

public class Hoge {
    /**
     * @param foo can noooooooooooooot be NULL!
     */
    public Hoge(Foo foo) {
        if (foo == null) {
            throw new NullPointerException("...");
        }
        this.foo = foo;
    }

こうすれば望まれないNullが設定された際には即座にNullPointerExceptionをスローすることができ, 犯人をスタックトレース上にあぶり出すことができるはずだ.
だが, これだとせっかくのNonNull Code Inspectionの恩恵を受けられない. 犯人をあぶり出すことができるようになっても, 誤ってNullを設定してしまう犯人(イージーなミス)が増えてしまいそうだ.

@NonNullアノテーションの恩恵は残しておきたい.
なので@NonNullアノテーションはそのまま残して, Nullチェックのコードを追加してみる.

    public Hoge(@NonNull Foo foo) {
        if (foo == null) {
            throw new NullPointerException("...");
        }
        this.foo = foo;
    }

些か奇妙なコードにも見える. そのせいかAndroid Studioも “Condition ‘foo == null’ is always ‘false’” と別のCode Inspectionをコメントしてきた.
確かにその通り(そうであって欲しいの)だが, 望まない副作用を避けるためにもこの場所にはNullチェックを書いておきたい事情があるのだ.
このまま放っておいても良いが, 常に警告されているのも気持ち良いものではないのでAndroid Studioには”そうではない”ことを伝えておく.

    public Hoge(@NonNull Foo foo) {
        // noinspection ConstantConditions
        if (foo == null) {
            throw new NullPointerException("...");
        }
        this.foo = foo;
    }

これで毎回 “些か奇妙なコード” をLint checkの度, 目にすることもなくなった.

毎回noinspectionを書くのが嫌なら, nonNullをチェックするutilメソッドを定義しておけばより簡素なコードにできる.

public static <T> T nonNull(T o) {
    if (o == null) {
        throw new NullPointerException("Require Non null object");
    }
    return o;
}

// this.foo = nonNull(foo);

ところで, 残ったメソッドpiyoの方は問題ないだろうか.

    public void piyo(@NonNull Foo f) {
        f.call();
    }

これは問題ないだろう. 引数fにNullを渡された場合でも潜伏期間なしにNullPointerExceptionがスローされ, スタックトレースには犯人が載っているはずだ.
fにNullを設定するとNullPointerExceptionがスローされる正しいコードだ.

まとめ

@NonNullはあくまでも補助的な機能であって非Nullを保証するものではない.
メソッドの引数にNullが渡るとNullPointerExceptionが発生することを明示する場合, @NonNullアノテーションはとても役に立つ.

本来Nullを許容しない(NullPointerExceptionをスローする)ことを明示する@NonNullアノテーションだが, Nullを受け取っても異常を報告せず終了してしまう@NonNullな引数を作ることもできてしまう.
そのようなケースでも, @NonNullが有益なケースはあるが, それによる副作用も考慮した上で, 必要なNullチェックは書いておこう.

以上.