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();
}
}
}
-
ガード節を表現する実装をする
- メソッド内の頭で、受け入れられない値が来たらデフォルト値や例外を投げるようにしてメソッドから抜けることで、考えなきゃいけないことを減らせる
java.util.Objects#requireNonNullとか、Apache Commons Lang のValidate#notNullとかのnullチェック系のユーティリティメソッドを使ってれば、さらに「あら、よくご存知ね」感上がります
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 のスライドを読みなさい運動を奨励しています。
-—-