8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【初心者】JavaのNullPointerExceptionの犯人探し

Posted at

はじめに

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が起きたかわかりますか?(複数回答可)

  1. new SampleObj()の結果
  2. getHoge()の戻り値
  3. doMoge()の戻り値
  4. 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かを懇切丁寧に教えてくれるようです。

8
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?