Menu

Java の NullPointerException: 原因と解決方法

Java の NullPointerException が本当は何を意味するのか、それを引き起こすよくあるパターン、メッセージの読み方、そしてそれを防ぐ書き方を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

Java で最もよくあるエラー

NullPointerException(みんな NPE と呼びます)は、何も指していない参照 -null- を、まるで本物のオブジェクトを指しているかのように使おうとしたときに発生します。オブジェクト型の Java 変数は、オブジェクトを保持しているか、「ここにオブジェクトはない」という値である null を保持しているかのどちらかです。null に対して何かをさせようとした瞬間 -- メソッドを呼ぶ、フィールドの一つを読む、添字でアクセスする -- 操作する対象が何もないため、JVM は例外をスローします。

前のページで想定された失敗を処理するために使う try-catch とは違い、NPE はほとんどの場合、単なるバグです。このページの目的はそれらをキャッチすることではなく、なぜ起きるのかを理解し、そもそもそれを生み出さないコードを書くことです。

length() を実行する String が存在しないため、プログラムは NullPointerException で停止します。

メッセージを読む

Java 14 以降、メッセージは何が null だったかを正確に教えてくれます。これは Helpful NullPointerException(役立つ NPE)と呼ばれます。何かを変更する前に、まずこれを読みましょう:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
	at Main.main(Main.java:4)

重要なのは 2 つの部分です。Cannot invoke "String.length()" は失敗した操作で、because "name" is null が犯人を名指ししています。at Main.main(Main.java:4) の行は、正確な行を指し示すスタックトレースです。つまり修正方法は「4 行目を try で囲む」ことではなく、「4 行目で name がなぜ null なのかを突き止める」ことです。バグはほぼ必ず、それより前の、値が代入されるべきだったのにされなかった場所にあります。

引き起こすさまざまなパターン

NPE はわずかな種類の操作から生じ、そのどれもが「null に触れる」ことのバリエーションです:

map のルックアップは典型的な原因です。キーが無いと getnull を返し、その値を実際に使う何行も後になって初めて NPE が表面化することがよくあります。アンボクシングは厄介なもので、nullIntegerint に代入すると、コピーすべき数値が存在しないため例外がスローされます。

null チェックで守る

最もシンプルな防御は、参照を使う前に null でないことを確認する if です:

変数を定数の String と比較するときは、定数を先に置きましょう。answer.equals("yes") ではなく "yes".equals(answer) です。answernull の場合、前者は穏やかに false を返しますが、後者は例外をスローします。

Objects.requireNonNull で早期に失敗させる

あちこちに null チェックを散りばめると煩雑になります。ある値が決して null であってはならないとき -- コンストラクタの引数などのように -- 境界で Objects.requireNonNull を使って検証しましょう。これは、あとでコードの奥深くで失敗するのではなく、不正な値が到着したその地点で、明確なメッセージとともに直ちに例外をスローします:

この「早期に失敗させる」習慣は、200 行も離れた曖昧な NPE を、発生源での的確な指摘に変えます。ここで例外をキャッチしているのはメッセージを表示するためだけです。実際のコードでは、バグが修正されるように例外をそのまま表面化させるでしょう。

そもそも null を避ける

最良の NPE は、ぶつかる null がそもそも存在しないために決して発生しえないものです。いくつかの習慣が大いに役立ちます:

  • null ではなく、空のコレクションや空文字列を返しましょう。Collections.emptyList()"" は、ループで回したりメソッドを呼んだりしても安全です。
  • map では getOrDefault を使い、見つからなかったルックアップが null ではなく実際の値を返すようにしましょう。
  • フィールドは「あとで」まで null のままにせず、宣言時に初期化しましょう。

値が本当に任意である場合 -- 正当に何も見つからないことがありうるルックアップなど -- Java は Optional を提供します。これは、こっそり null を返す代わりに、呼び出し側に「不在」のケースを処理させることを強制するコンテナです。API からこうした穴を設計レベルで完全になくしたいなら、次に読むべき関連概念がこれです。

まとめ

NullPointerException は、null を保持している参照を、オブジェクトを保持しているかのように使ったと Java が告げているものです。修正方法はそれをキャッチすることではほとんどありません。役立つメッセージを読み、値が設定されるべきだった場所までさかのぼり、null でないことを保証するか、それを使う箇所を守ることです。境界で早期に失敗させるために Objects.requireNonNull を活用し、null よりも空の値や getOrDefault を選び、不在が現実的で想定された結果である場合には Optional に頼りましょう。この考え方を身につければ、Java で最もよくあるエラーは、あなた自身のコードでは最も稀なものの一つになります。

よくある質問

Java で NullPointerException は何が原因で起きますか?

null を指している参照を、まるで本物のオブジェクトを指しているかのように使うと発生します。たとえばメソッドを呼ぶ (name.length())、フィールドを読む、配列の要素にアクセスする、nullInteger をアンボクシングする、などです。変数はオブジェクトを保持していないため、操作する対象が存在せず、JVM は NullPointerException をスローします。

Java で NullPointerException はどう直しますか?

まずメッセージを読みましょう。Java 14 以降は何が null だったかを正確に示します(例: "Cannot invoke "String.length()" because "name" is null")。次に、その変数がなぜ null なのかをたどります。初期化漏れ、null を返したメソッド、何も見つからなかった map のルックアップなどです。値が決して null にならないように原因を直すか、null チェック、Objects.requireNonNullOptional で使用箇所を守りましょう。

null をチェックするのと NullPointerException をキャッチするのとではどちらが良いですか?

null をチェックしてください。NullPointerException は想定された状況ではなく、ロジック上のバグを示すものなので、キャッチするのではなく防ぐべきです。キャッチすると、本当の問題がどこにあるのか分かりにくくなります。try/catch は、入出力エラーのような本当に例外的な状況のために取っておきましょう。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める