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 のルックアップは典型的な原因です。キーが無いと get は null を返し、その値を実際に使う何行も後になって初めて NPE が表面化することがよくあります。アンボクシングは厄介なもので、null の Integer を int に代入すると、コピーすべき数値が存在しないため例外がスローされます。
null チェックで守る
最もシンプルな防御は、参照を使う前に null でないことを確認する if です:
変数を定数の String と比較するときは、定数を先に置きましょう。answer.equals("yes") ではなく "yes".equals(answer) です。answer が null の場合、前者は穏やかに 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())、フィールドを読む、配列の要素にアクセスする、null の Integer をアンボクシングする、などです。変数はオブジェクトを保持していないため、操作する対象が存在せず、JVM は NullPointerException をスローします。
Java で NullPointerException はどう直しますか?
まずメッセージを読みましょう。Java 14 以降は何が null だったかを正確に示します(例: "Cannot invoke "String.length()" because "name" is null")。次に、その変数がなぜ null なのかをたどります。初期化漏れ、null を返したメソッド、何も見つからなかった map のルックアップなどです。値が決して null にならないように原因を直すか、null チェック、Objects.requireNonNull、Optional で使用箇所を守りましょう。
null をチェックするのと NullPointerException をキャッチするのとではどちらが良いですか?
null をチェックしてください。NullPointerException は想定された状況ではなく、ロジック上のバグを示すものなので、キャッチするのではなく防ぐべきです。キャッチすると、本当の問題がどこにあるのか分かりにくくなります。try/catch は、入出力エラーのような本当に例外的な状況のために取っておきましょう。