はじめに
Javaの開発者なら誰でも見覚えのあるNullPointerException。初心者がNullPointerExceptionに遭遇すると、見当違いな部分を直してしまったり、デバッグに時間をかけてしまうことがあります。また、ある程度Javaに慣れた方でも、なんとなくでNullPointerExceptionに対応している方もいるのではないでしょうか。
この記事では、NullPointerExceptionが発生した際に、どのオブジェクトがnullでNullPointerExceptionが起きているのか、スタックトレースとソースコードのみで特定する方法をまとめました。
問題
さて、まずは問題です。下記にNullPointerExceptionが発生したソースとスタックトレースを記載しました。Java11を使って確認しています。
public class NPESample {
public static void main(String[] args) {
new SampleObj().getHoge().doMoge(getFuga());//NullPointerException
}
/*以下略*/
Exception in thread "main" java.lang.NullPointerException
at NPESample.main(NPESample.java:5)
何がnullでNullPointerExceptionが起きたかわかりますか?(複数回答可)
- new SampleObj()の結果
- getHoge()の戻り値
- doMoge()の戻り値
- getFuga()の戻り値
正解
正解は2のみです。
これに自信をもって回答できなかった方が、この記事の対象読者となります。
基本パターン:ピリオドの左がnull
まず、NullPointerExceptionの定義についてですが、JavaDocには下記のようにあります。
オブジェクトが必要な場合に、アプリケーションがnullを使おうとするとスローされます。たとえば、次のような場合があります。
nullオブジェクトのインスタンス・メソッドの呼出し。
nullオブジェクトのフィールドに対するアクセスまたは変更。
nullの長さを配列であるかのように取得。
nullのスロットを配列であるかのようにアクセスまたは修正。
nullをThrowable値であるかのようにスロー。
https://docs.oracle.com/javase/jp/8/docs/api/java/lang/NullPointerException.html
このうち最頻なのが、nullオブジェクトのインスタンス・メソッドの呼出し
だと思います。
これはソースコード上ではnullのオブジェクト.メソッド
の形になります。
簡単に言うと、NullPointerExceptionでは、 ピリオドの左側がnull だということです。
これを覚えておけばNullPointerExceptionの9割は犯人を推察することができます。
new SampleObj().getHoge().doMoge(getFuga());
ピリオドの左側がnull の法則に照らし合わせると、回答は1. new SampleObj()の結果
2. getHoge()の戻り値
に絞り込めます。
では、1. new SampleObj()の結果
はなぜ違うのか。
Javaのnewは生成したオブジェクトを返すか、例外をスローするかのどちらかしかなく、nullを返してくることはありません。そのため、2が回答となります。
なお、nullオブジェクトのフィールドに対するアクセスまたは変更。
はnullのオブジェクト.フィールド
ですし、nullの長さを配列であるかのように取得
はnullのオブジェクト.length
ですので、これらもピリオドの左側がnullに当てはまります。
スタックトレースの最上段に注目
4. getFuga()の戻り値
を選択してしまった方もいるかと思うのですが、
getFuga()の戻り値がnullだったとしても、getFugaにはピリオドはついていないためすぐにNullPointerExceptionは起きません。
起きるとしたら、doMoge(getFuga())
のdoMogeの中で引数に対して何かをしている場合ですが、その場合は、スタックトレースは下記のようになります。
Exception in thread "main" java.lang.NullPointerException
at SampleObj$Hoge.doMoge(SampleObj.java:10)
at NPESample.main(NPESample.java:5)
最上段が
at NPESample.main(NPESample.java:5)
ではなく、 at SampleObj$Hoge.doMoge(SampleObj.java:10)
になっており、doMogeの中でNullPointerExceptionが起きたことになります。
今回の問題は、at NPESample.main(NPESample.java:5)
でしたので、4は違うことがわかります。
このように、NullPointerExceptionの調査では、 スタックトレースの最上段の行 を見るのが重要です。最上段の行をソースから探し、 ピリオドの左側がnull になる可能性がある場所を探せば、なにがnullなのかがわかります。
その他のパターン
ここからは ピリオドの左側がnull 以外の代表的なNullPointerException発生パターンを記載します。
角括弧の左がnull
nullのスロットを配列であるかのようにアクセスまたは修正。
に該当すると思われますが、
a[0]
のような箇所でaがnullだとNullPointerExceptionが発生します
拡張for文
for(Sample sample : sampleList) {
のような拡張for文でsamleListがnullだとNullPointerExceptionが発生します。
内部でsamleList.iterator()を呼んでいるからと理解すれば納得できますね。
primitiveへのキャスト
Integer integer = null;
int i = (int)integer;
このようにラッパー型のnullをprimitive型にキャストする場合もnullが起きます。
オートボクシングの場合も同様です。
まとめ
NullPointerExceptionに出会ったら、 スタックトレースの最上段 のソースを見て ピリオドの左側がnull を真っ先に疑うようにしましょう。
これを知っていれば、デバッグのできない環境でも、スタックトレースとソースコードだけで、NullPointerExceptionの犯人につかづくことができます。
おまけ
Java14からは、NullPointerExceptionのメッセージが親切になり、何がnullかを懇切丁寧に教えてくれるようです。