NullPointerExceptionのメッセージの改善

Java 14に追加された新機能の1つに、JEP 358として提案された「Helpful NullPointerExceptions」がある。日本語にすると「親切なNullPointerExceptions」で、簡単に言ってしまえばNullPointerException発生時に出力されるメッセージがわかりやすくなるというものだ。

  • JEP 358: Helpful NullPointerExceptions

    JEP 358: Helpful NullPointerExceptions

NullPointerExceptionは、Javaプログラマーであれば誰でも高い頻度で目にする例外の1つと言えるだろう。NullPointerExceptionはプログラム内のどこでも発生する可能性があるため、そのすべてをtry/catchで捕捉して対処しようというのは現実的ではない。したがって、一般的にはNull参照を行わないように注意してプログラムを組むが、それでもNullPointerExceptionが発生してしまった場合には、その対応はJVMによって生成されるメッセージを頼りに問題箇所を絞り込んで行うことになる。

しかし、そのような重要な役割を担っているにも関わらず、従来のNullPointerExceptionのメッセージは情報量が少なく、例外の発生箇所を特定するには十分とは言えなかった。たとえば次のようなコードがあったとする。

package jp.mynavi.imajava.npe;

import java.util.ArrayList;
import java.util.List;

public class NPETest {
    public static void main(String... args) {
        String str = null;
        List<String> list = new ArrayList<>();
        list.add(str);
        list.get(0).length();
    }
}

このコードを実行すると次のようになる。

Exception in thread "main" java.lang.NullPointerException
    at jp.mynavi.imajava.npe.NPETest.main(NPETest.java:11)

NPETest.javaというファイルの11行目(list.get(0).length()の行)でNullPointerExceptionが発生したことはわかるが、これだけではlistがnullなのか、list.get(0)の戻り値がnullなのかを特定することはできない。

これに対してJava 14の親切なNullPointerExceptionsの場合、メッセージの内容は次のように改善されている。

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "java.util.List.get(int)" is null
    at jp.mynavi.imajava.npe.NPETest.main(NPETest.java:11)

このメッセージであれば、list.get(0)の戻り値がnullなためにNullPointerExceptionが発生したことがすぐにわかる。

親切なNullPointerExceptionsを有効にする

Java 14を使っていても、デフォルトでは親切なNullPointerExceptionsは有効になっていない。親切なNullPointerExceptionsを有効にするには、次のように実行時のコマンドラインオプションに -XX:+ShowCodeDetailsInExceptionMessages を付加することで有効にできる。

java -XX:+ShowCodeDetailsInExceptionMessages jp.mynavi.imajava.npe.NPETest

親切なNullPointerExceptionsの例

いくつかのパターンで、親切なNullPointerExceptionsのメッセージを見てみよう。まず、次のようなコードの場合はどうだろうか。

public class Hoge {
    public Piyo piyo;
    public Hoge(Piyo piyo) {
        this.piyo = piyo;
    }
}

public class Piyo {
    public String msg;
    public Piyo(String msg) {
        this.msg = msg;
    }
}

String str = null;
Piyo piyo = new Piyo(str);
Hoge hoge = new Hoge(piyo);
int length = hoge.piyo.msg.length();    // NullPointerException

この場合、「"hoge.piyo.msg" is null」というように、NullPointerExceptionが発生した場所への完全なアクセスパスが表示される。したがって、明確にPiyoインスタンスのmsgプロパティがnullであることがわかる。

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "hoge.piyo.msg" is null
    at jp.mynavi.imajava.npe.NPETest2.main(NPETest2.java:13)

次のコードのように、配列の場合はどうだろうか。

String[][][] strs = new String[5][][];
strs[0] = new String[2][];
strs[0][1][2] = "Hello";    // NullPointerException

この場合、strs[0][1]への代入でNullPointerExceptionが発生するので、「"strs[0][1]" is null」というように明示される。

Exception in thread "main" java.lang.NullPointerException: Cannot store to object array because "strs[0][1]" is null
    at jp.mynavi.imajava.npe.NPETest3.main(NPETest3.java:8)

次のコードでは、「fuga2.num = fuga1.num」の箇所でNullPointerExceptionが発生する。

public class Fuga {
    public int num;
}

Fuga fuga1 = new Fuga();
Fuga fuga2 = null;
fuga1.num = 123;
fuga2.num = fuga1.num;  // NullPointerException

この場合でも、次のようにfuga2のほうでnull参照が行われたことが明示される。

Exception in thread "main" java.lang.NullPointerException: Cannot assign field "num" because "fuga2" is null
    at jp.mynavi.imajava.npe.NPETest4.main(NPETest4.java:11)

次のようなコードでは、list.add()とstr.substring()のいずれでもnull参照が行われている。

String str = null;
List<String> list = null;
list.add(str.substring(0,5));  // NullPointerException

このケースでは、str.substring()の方で先にNullPointerExceptionが発生し、list.add()までは到達しないので、必然的にstr.substring()のnull参照のみが報告される。

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.substring(int, int)" because "str" is null
    at jp.mynavi.imajava.npe.NPETest5.main(NPETest5.java:10)

親切なNullPointerExceptionsのデメリット

親切なNullPointerExceptionsは極めて実用的だが、デメリットもある。JEP 358では、次のような理由から、親切なNullPointerExceptionsが常に望ましいとは言えないと指摘されている。

  • パフォーマンス: スタックトレースを生成するオーバーヘッドが発生する。ただし、例外発生時に実行されるスタック・ウォーキングと同程度なので、頻繁に例外が発生する状況でない限り大きな影響はない。
  • セキュリティ: 親切なNullPointerExceptionsで出力されるメッセージは、通常は公開されないソースコード情報を含んでいる。情報の公開が許容できない場合は、アプリケーション側でメッセージを表示しないように処理する必要がある。
  • 互換性: メッセージのフォーマットが新しくなったことで、スタックトレースを解析するツールなどにおいて問題が発生する可能性がある。

これらのデメリットが許容できない状況では、親切なNullPointerExceptionsは使用しないほうがいいだろう。

親切なArrayIndexOutOfBoundsExceptions

今回は例外メッセージの話だったので、ついでにArrayIndexOutOfBoundsExceptionについても取り上げておきたい。実は、NullPointerExceptionだけでなくArrayIndexOutOfBoundsExceptionのメッセージも少しだけ親切になっている。この変更はJava 11ですでに有効になっていたもので、NullPointerExceptionと違って実行時にコマンドラインオプションを追加しなくても自動で適用される。

例えば次のコードの場合、arrayの1次元目の要素数は3なので、array[10][1]の参照でArrayIndexOutOfBoundsExceptionが発生する。

int[][] array = {{1,2},{3,4},{5,6}};
System.out.println(array[10][1]);

これをJava 8で実行した場合の例外メッセージでは、次のように単に「10」とだけ表示される。これでは、初心者には何が「10」なのかがわかりにくい。

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
    at AIOOBETest.main(AIOOBETest.java:4)

Java 11以降では、このメッセージが次のように変更されている。

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3
    at AIOOBETest.main(AIOOBETest.java:4)

このメッセージであれば、長さ3の配列に対しての10番目の要素へのアクセスは"Out Of Bounds"だということが一目で理解できる。

親切なNullPointerExceptionsやArrayIndexOutOfBoundsExceptionsは、新機能と言うには地味ではあるが、非常に便利な改善と言える。特に開発の段階では、常に -XX:+ShowCodeDetailsInExceptionMessages を付けて親切なNullPointerExceptionsを有効にして使うことをお勧めしたい。