新規広告開発部の松本です。 クックパッドiOS/Androidアプリの広告の開発に携わっています。
Androidアプリ開発の際、皆さんはnullをどのように扱っていますか?また、nullチェックを行うのであれば、どのような基準で行っていますか?私自身まだまだAndroid開発歴が浅いため、特に何か基準がある訳でもなく至る所でif (foo != null)
といったnullチェックを行おうとしていました。
これに対し、先日の社内コードレビューでとてもためになるアドバイスをもらいました。私のようなAndroid初心者にとってnullに対する考え方の基礎を作ってくれるレビューだったので、本稿で共有したいと思います。
また、AndroidやJava開発に慣れた方にとっては「今更そんな話か」といった内容かと思いますが、クックパッドでのレビューの一例としてご覧いただければ幸いです。
やりがちなnullチェック
あるとき、「Androidではクラッシュしないことが大事であり、そのためにnullチェックは基本!」という考えのもと、以下のようなコードを至るところに書きました。
if (foo != null) { foo.bar(); }
するとコードレビューでこんなコメントをもらいました。
一般に、過剰なエラーチェックは混乱を招くのでよくないし、内部状態が壊れているのになんとなく動いてしまうというのは結果が予測できず、クラッシュより深刻なバグになる可能性があるのでよくないです。
今まで考えていた事とは逆ですが、確かにレビュアーの言うとおりです。歪な修正は根本的な原因を隠す*1ことに繋がるのですね。
では、どんな基準でnullチェックを行えば良いのでしょうか? そんな疑問が浮かんだ頃、こんなアドバイスをもらいました。
- nullはふつうにあるし、無視でよい
- -> 現状この振る舞いでOK
- nullは想定内の例外なのでハンドルしたい
- -> error listenerを登録できるようにする
- nullは完全に想定外なので、APIのバグでしか起こりえない
- -> IllegalStateExceptionでクラッシュさせる。またその結果Crashlyticsにログが記録される
nullチェックだけでなく、try catch文やCrashlyticsを使う基準までまとめてもらいました。「敢えてクラッシュする可能性を残す」ことも時には必要なんですね。
@Nullableと@NonNull
またあるとき、こんなコードを書きました。
public class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } }
これに対し、こんなレビューをもらいました。
engineについて @Nullable or @NonNull annotationがほしいすね。nullがくるのが仕様なのかそうでないか知りたい。
そもそも仕様としてnullはこない(くるべきでない)なら、引数を @NonNull で注釈しつつnullがきたらIllegalArgumentException投げるみたいなのでもいいんですよ。
そんなアノテーションも使えるんですね!どうやらAndroid Support library version 19.1からサポートしているようです*2。
更にこんなアドバイスも。
引数にも @NonNull Engine engine と指定することができて、そうしているとこのメソッドを呼び出すときに @Nullable な値を渡そうとするとIDEが警告出してくれたりするんですよ!まあ、警告はかならず出してくれるわけではないので、documentの意味合いのほうが強いですけど。
@Nullable などはどの変数やフィールドにつけてもそれなりに効果はありますが、一番効果がある(ゆえに優先度が高い)のは公開インターフェイス部分です。つまり引数と戻り値すね。
引数に指定してあると「あーnull渡すとだめなんだ」ってすぐわかるし、戻り値がnullableだと someMethod().chainedMethod() みたいにすると警告が出るのでわかりやすい。とはいえとりあえずは引数だけつけるのがおすすめです。
Android Studioはversion 0.5.5から@Nullable/@NonNull対応、すなわち警告を出してくれるようですね*3。 引数に@NonNullを指定するのはとても便利ですね。ライブラリやSDKを作る際に、「この引数はnull禁止!」ということを明示的に示すことで想定外の使われ方が減りそうです。
正しくnullを扱う例
以上のアドバイスを元に以下のコードを修正してみましょう。
public class CarMaker { public Car make(Engine engine) { Car car = new Car(engine); car.checkEngine(); return car; } private class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } public void checkEngine() { if (engine != null) { engine.check(); } } } }
コード中のengine変数はnullを許容できないので、CarMakerクラスの利用者にその旨を通知したいとします。
そこで、engine変数は@NonNullであることをメソッドの引数で宣言します。
public class CarMaker { public Car make(@NonNull Engine engine) { Car car = new Car(engine); car.checkEngine(); return car; } private class Car { private Engine engine; public Car(@NonNull Engine engine) { this.engine = engine; } public void checkEngine() { engine.check(); } } }
checkEngine()メソッド内のnullチェックがなくなりスッキリし、可読性が上がりました。
また、engineはnullを受け入れないことが明示的に宣言されたので、アノテーション対応のIDE*4を使用した際に警告が表示されるようになりました。
余談
実はこのnullの扱い、そう単純に割り切れないようです。 いくつか考慮すべき点があります。
どこまで@NonNullを付けるか
上の例では各メソッドの引数にのみ@NonNullを付けました。 しかし、Carクラスのengineメンバなどにも@NonNullを付けることは可能です。 どこまで@NonNullを付けて、どこからは付けなくてよいのでしょうか?
厳格に考える場合
厳格に考えればCarのメンバであるengineにも@NonNullを付加すべきでしょう。 理由は以下のとおりです。
- public methodの引数はドキュメントとしても、IDEに対する注釈としても有効なので付ける
- さらに、@Nullable/@NonNullは付ければ付けるだけ恩恵が広がるのでなるべく付ける
public class CarMaker { public Car make(@NonNull Engine engine) { Car car = new Car(engine); car.checkEngine(); return car; } private class Car { @NonNull private Engine engine; public Car(@NonNull Engine engine) { this.engine = engine; } public void checkEngine() { engine.check(); } } }
緩く考える場合
緩く考えれば、@NonNullを付加するのはmake()メソッドだけで十分でしょう。 理由は以下の通りです。
- CarはCarMakerの内部クラスなので、CarMaker#make() にNonNullを付ければ十分
public class CarMaker { public Car make(@NonNull Engine engine) { Car car = new Car(engine); car.checkEngine(); return car; } private class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } public void checkEngine() { engine.check(); } } }
何の例外を投げるか
これまでの例では明示的に例外を投げていないので、NullPointerExceptionが発生します。 しかし、「引数が不正なのだからIllegalArgumentExceptionを投げるべきだ」と考える事もできます。
その場合、コードは以下のようになるでしょう。 @NonNullは一切付加せず、engineに対してnullチェックをしています。
public class CarMaker { public Car make(Engine engine) { if (engine == null) { throw new IllegalArgumentException("Engine must not be null."); } Car car = new Car(engine); car.checkEngine(); return car; } private class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } public void checkEngine() { engine.check(); } } }
どこまで使用者を信頼するか
「どこまで@NonNullを付けるか」と「何の例外を投げるか」、以上の2つは「どこまでクラス使用者(今回はCarMaker使用者)を信頼するか」に依るところもあるようです。
例えばCarMakerを使うのは自分だけであれば、make()メソッドの引数に@NonNullを付加してnullを与えないよう気をつけるだけでよいでしょう。
一方、CarMakerをライブラリとして公開し不特定多数の人が使用するのであれば、make()メソッドの引数で@NonNullを付加し注意を促した上で、Carクラス内でIllegalArgumentExceptionを投げることもあるでしょう。
public class CarMaker { public Car make(@NonNull Engine engine) { Car car = new Car(engine); car.checkEngine(); return car; } private class Car { private Engine engine; public Car(Engine engine) { if (engine == null) { throw new IllegalArgumentException("Engine must not be null."); } this.engine = engine; } public void checkEngine() { engine.check(); } } }
まとめ
本稿では、Android開発において私自身が受けたレビューを元に、nullの扱いについてお話しました。 一見簡単ながら、知れば知るほど奥が深く、私も引き続き勉強したいと思います*5。
私と同じく最近Android開発を始めた方のお役に少しでも立てば幸いです。