Java で引数の null チェックで迷った話
これは Java Advent Calendar 2015 の 15 日目の記事です。
昨日は @opengl_8080 さんの Byteman 使い方メモ+α でした。明日は @irof さんです。
前置き
ついこないだチームでちょっとだけ話題に上って、みんなある程度指針は持っているものの、割と悩みつつ明確に答えを出せなかったので、もっと良い意見があればと思って晒してみます。まぁよくある話だし、Java 8 で Optional
が使えるようになって null
について語られるケースが増えたと思うので、再考するちょうどよい機会になればいいなーと思います。初心者向けです。
どう処す?処す?
こんな状況の時にあなたならどうしますか?
// Generics なのは例です。String でもなんでもいいです public T doSomething(T input) { // input が null の時にどう処す?処す? }
もちろん、呼び出し側のコンテキストとか、ライブラリを何使うかとか、Java 8 or それ以前とか、そういった前提によっていろいろ対処は変わってくるとは思うのですが、いくつか選択肢があると思います。良い悪いを置いといて、よく見るのは、
null
を返す- 何らかのデフォルト値を返す
java.lang.IllegalArgumentException
などの例外を投げる
辺りかなぁと思います。null
を返すパターンの時は、もし返り値が Collection
や配列なのに、null
返しちゃうようなのを見かけたら、Effective Java をそっと差し出してあげてください。
おぬし気が効いてるのう
個人的に、以下のような実装を見たら、おお、おぬしホスピタリティを心得ておるのぅ、って思います。
- Null Object パターンで何もしないオブジェクトを返す
- これも一種のデフォルト値を返すパターンと言える
public interface Command { void execute(); } public class ABCCommand implements Command { public void execute() { System.out.println("ABC"); } } // こいつが Null Object public class NullCommand implements Command { public void execute() { // do nothing } } public class Main { public static void main(String... args) { Command missing = createCommand(null); // 普通こんなことやらないけど missing.execute(); // createCommand の戻り値が null だったら〜という null チェックが要らない } public static Command createCommand(String name) { if ("abc".equals(name)) { return new ABCCommand(); } else { return new NullCommand(); } } }
- ガード節を表現する実装をする
public T doSomething(T input) { if (input == null) { throws new IllegalArgumentException("null はダメよ〜ダメダメ"); } // ここから input != null の場合の処理 } public T doSomething(T input) { Objects.requireNonNull(input); // ここから input != null の場合の処理 }
- Java 8 使ってるなら
- ガード節でも良いけど、
java.util.Optional
のifPresent()
やorElse()
でnull
の時を無視したり、デフォルト値を返すようにする - 「最初に受け入れない値を弾く」というよりは、「受け入れる時だけ処理する」とか「受け入れない奴は後で
orElse()
ね」、みたいなメンタルモデルの転換が必要かもしれない
- ガード節でも良いけど、
public String capitalize(String input) { return Optional.ofNullable(input).map(String::toUpperCase).orElse(""); // null の時は空文字が返る } public void printCapitalize(String input) { Optional.ofNullable(input).ifPresent(s -> System.out.println(s.toUpperCase())); // null じゃない時だけ処理する } // これはアカン public String capitalize(String input) { Optional<String> optionalInput = Optional.ofNullable(input); if (optionalInput.isPresent()) { return ""; } return optionalInput.map(String::toUpperCase).get(); }
ここから本題
で、私としては、引数が null
の時に前提条件を満たしていないケースでデフォルト値を返さなくて良い場合は、IllegalArgumentException
(またはアプリケーション固有の前提条件エラーを表現する実行時例外)を投げることが多いです。上の例で挙げた、
public T doSomething(T input) { if (input == null) { throws new IllegalArgumentException("null はダメよ〜ダメダメ"); } // ここから input != null の場合の処理 } public T doSomething(T input) { Objects.requireNonNull(input); // ここから input != null の場合の処理 }
このパターンですね。
で、Java 7 から使えるようになった java.util.Objects#requireNonNull
便利やなーと思って実装を見ていたら、
public static <T> T requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; } public static <T> T requireNonNull(T obj, String message) { if (obj == null) throw new NullPointerException(message); return obj; }
こうなってるんです。IllegalArgumentException
ではなくて、NullPointerException
を返してるんですね。しかし今まで私は IllegalArgumentException
派だったんです。理由は、
- 引数が
null
なのが自明なので、自分でわざわざNullPointerException
投げるのが冗長だしなんか抵抗がある - ぬるぽが起きたところでぬるぽだよ、ってメッセージを出すよりも、
IllegalArgumentException
でnull
は受け入れないよ、というメッセージの方がより前提条件をはっきり主張していると思う
ただ、メリットもあって、java.util.Objects#requireNonNull
の Javadoc にも書いてありますが、コンストラクタで使うと実際のメソッド呼び出し時より早く NullPointerException
かどうかが分かる、ということもあります。
public Foo(Bar bar) { this.bar = Objects.requireNonNull(bar); }
そんな感じで、どっちがいいのか迷ってしまったので、とりあえず他の実装を見てみました。
見てみたのは以下の4つ。
- Apache Commons Lang 2系(v2.6)の
Validate#notNull()
- Apache Commons Lang 3系(v3.4)の
Validate#notNull()
- Google Guava v19.0 の
Preconditions#checkNotNull()
- Spring Framework Core の
Assert#notNull()
Apache Commons Lang 2系(v2.6)の Validate#notNull()
public static void notNull(Object object) { notNull(object, "The validated object is null"); } public static void notNull(Object object, String message) { if (object == null) { throw new IllegalArgumentException(message); } }
Apache Commons Lang 3系(v3.4)の Validate#notNull()
private static final String DEFAULT_IS_NULL_EX_MESSAGE = "The validated object is null"; // 途中省略 public static <T> T notNull(final T object) { return notNull(object, DEFAULT_IS_NULL_EX_MESSAGE); } public static <T> T notNull(final T object, final String message, final Object... values) { if (object == null) { throw new NullPointerException(String.format(message, values)); } return object; }
Google Guava v19.0 の Preconditions#checkNotNull()
public static <T> T checkNotNull(T reference) { if (reference == null) { throw new NullPointerException(); } return reference; } public static <T> T checkNotNull(T reference, @Nullable Object errorMessage) { if (reference == null) { throw new NullPointerException(String.valueOf(errorMessage)); } return reference; }
Spring Framework Core の Assert#notNull()
public static void notNull(Object object) { notNull(object, "[Assertion failed] - this argument is required; it must not be null"); } public static void notNull(Object object, String message) { if (object == null) { throw new IllegalArgumentException(message); } }
実際に実行してみる
おまけ:Lombok
Lombok にも @NonNull
という null
を許容しないことを表すアノテーションがあるので、それも一緒に実行してみました。
@Data public class NullObject { private String name; public NullObject(@NonNull String name) { this.name = name; } }
テストコード
public class NullTest { @Test public void caseOfRequireNonNull() throws Exception { Objects.requireNonNull(null); } @Test public void caseOfCommonsLang2Validate() throws Exception { org.apache.commons.lang.Validate.notNull(null); } @Test public void caseOfCommonsLang3Validate() throws Exception { org.apache.commons.lang3.Validate.notNull(null); } @Test public void caseOfGuava() throws Exception { com.google.common.base.Preconditions.checkNotNull(null); } @Test public void caseOfSpring() throws Exception { org.springframework.util.Assert.notNull(null); } @Test public void caseOfLombok() throws Exception { new NullObject(null); } }
実行結果
java.lang.NullPointerException at java.util.Objects.requireNonNull(Objects.java:203) at MainTest.caseOfRequireNonNull(MainTest.java:13) java.lang.IllegalArgumentException: The validated object is null at org.apache.commons.lang.Validate.notNull(Validate.java:192) at org.apache.commons.lang.Validate.notNull(Validate.java:178) at MainTest.caseOfCommonsLang2Validate(MainTest.java:23) java.lang.NullPointerException: The validated object is null at org.apache.commons.lang3.Validate.notNull(Validate.java:222) at org.apache.commons.lang3.Validate.notNull(Validate.java:203) at MainTest.caseOfCommonsLang3Validate(MainTest.java:18) java.lang.NullPointerException at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:212) at MainTest.caseOfGuava(MainTest.java:28) java.lang.IllegalArgumentException: [Assertion failed] - this argument is required; it must not be null at org.springframework.util.Assert.notNull(Assert.java:115) at org.springframework.util.Assert.notNull(Assert.java:126) at MainTest.caseOfSpring(MainTest.java:33) java.lang.NullPointerException: name at NullObject.<init>(NullObject.java:11) at MainTest.caseOfLombok(MainTest.java:39)
まとめると
ライブラリ | 結果 |
---|---|
Java 7 の java.util.Objects の Objects#requireNonNull() |
NullPointerException |
Apache Commons Lang 2系(v2.6)の Validate#notNull() |
IllegalArgumentException |
Apache Commons Lang 3系(v3.4)の Validate#notNull() |
NullPointerException |
Google Guava v19.0 の Preconditions#checkNotNull() |
NullPointerException |
Spring Framework Core の Assert#notNull() |
IllegalArgumentException |
Lombok の @NonNull |
NullPointerException でエラーメッセージで null だったフィールドを教えてくれる(設定で IllegalArgumentException にもできる模様) |
見事に割れました。で、commons-lang の 2 系は古いので無視したとして、Spring の Assert#notNull()
も core パッケージに居ることもあってどちらかと言うとフレームワーク内部のためのクラスのような気もするので、そうすると NullPointerException
優勢な感じなのかなぁ、なんて思ったり。
チームの人にも聞いてみた
冒頭でも述べたとおり、チームの人にも聞いてみたのでチャットログを晒してみます。いろいろ意見が聞けました。基本サーバサイドの人が多いですが、A さんは Android ディベロッパなのでまた観点が違いますね。
(わたし): Java で引数が null かどうかチェックする時、NPE か IllegalArgumentException か、どっち投げます? 私 IAE 派だったんですけど、Java7 から入った Objects#requireNonNull が NPE 返すようになってて、どっちがええんやろなーと思った次第 ちなみに Apache commons lang (Validate#notNull)だと2系は IAE、3系は NPE Google guava の Preconditions#checkNotNull は NPE Spring Framework の Assert#notNull は IAE 悩ましい Lombok の @NonNull も NPE だなー NPE の方が主流なのか…なんか自分で NPE 投げるの抵抗あるんだよなぁw (Aさん): あかん。。。 AndoridStudioの@NonNull,@Nullableに慣れすぎていてnullバリデーションは放置状態の感覚になっている。。。 (Bさん): Effective Javaってどっち派でしたっけw なんか触れてる項ありましたよね ちなみに僕もIllegalArgumentExceptionでしたねw (わたし): ですよねw IDE任せにするなら @NonNull 使うのがいいのかなーやっぱり Effective Java は null 返すんじゃなくて空返せ、とかそんなんでしたっけ (Bさん): 空返せもありましたが、それとは別の項っす 気になるw http://tbpgr.hatenablog.com/entry/20130203/1359914058 これだ! NullPointerExceptionだったwww (わたし): Effective Java が言うんなら間違いない(迫真 (Bさん): 後ろ盾感ハンパないですね (わたし): 理由としてはぬるぽはぬるぽとして表現せよってことなのかなー(IAE と混ぜるな危険的な) (Cさん): あぁ… Springの Assert って例外投げるのか… なんか assert の構文とは違うものという認識が薄かったみたいですワタシ的には (わたし): それ罠っぽいすよねw (Cさん): 今知れてよかったw IAE > NPE 的な感じ?で使えば良さそう?なんすかね nullのケースはNPEがいいよ的な (わたし): そんな感じっぽいすな (Aさん): 盲目的にNPEというよりメソッドのレイヤーにもよる気がするんですけどね〜。 API的な動作をする場合、他の引数不正をIAEでやっているのにNonNull時だけNPEっていうのも使いにくい時ありそうな印象があります。 (わたし): サーバサイドだと、最終的にフレームワークの非チェック例外で丸めちゃう事が多いので、どっちかというとメッセージがちゃんとしてるかどうかのほうが重要なのかもしれないですね (Cさん): 個人的にはnull以外のケースと区別するって意味では良さそうに感じますけど、自前でvalidation系の例外投げたくなるケースとかにどうしようって思いそう >メッセージがちゃんとしてるかどうかのほうが重要 確かに (Aさん): >メッセージがちゃんとしてるかどうかのほうが重要 なるほど〜 (Cさん): あとは Effective Java 的にはちゃんとドキュメント書けしってよく言われてるところを見るに https://docs.oracle.com/javase/jp/8/api/java/util/Map.html containsKeyとかのドキュメントにはちゃんとNPEの出るケースが書かれてるので こういうのに倣っていくのが良いんですかね (わたし): おー、ほんまや put とかちゃんと使い分けてるのか…今さら知ったw https://docs.oracle.com/javase/jp/8/api/java/util/Map.html#put-K-V- (Cさん): おお!putいい例っぽい (Aさん): この例すごく解りやすいですね! あとは引数nullチェックしてNPEをわざわざthrowする気持ち悪さ感と throw new NullPointerException("IllegalArgument"); と書きたくなる誘惑との戦い・・・。
まとめ
Objects.requireNonNull
など、null
チェックを行うライブラリの実装はNullPointerException
優勢- Java8 で
Optional
が使えるようになったので、ifPresent()
やorElse()
を上手く使えるようになっておく - IDE のチェックや JSR-305 に任せるのも一興(
@NonNull
とか@Nullable
とか) - そのコードがライブラリなのかアプリケーションなのかによっても対処は違ってくる
- エラーメッセージをちゃんと書きましょう
- Javadoc にちゃんと書きましょう(Effective Java にもそう書いてある)
NullPointerException
とIllegalArgumentException
を混ぜない方がよい?- 例として
java.util.Map<K, V>#put()
は使い分けている
ご意見賜りたく
ケースバイケースだと思うので、全てのケースで同一の対処で済ませることはできないと思いますが、こういう時はこうすべき、というようなご意見があったら、コメント等で教えてくれたら嬉しいです。
あと、私としてはもし後輩に null
関連の質問をされたら、まずは太一( @ryushi )さんの JJUG CCC のスライドを読みなさい運動を奨励しています。