信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
nullとは何か — NullPointerExceptionが起きる根本理由と向き合い方
nullとは何か、なぜ存在するのか、なぜNullPointerExceptionが起きるのかを参照の仕組みから解説。Optionalの存在理由と、nullと安全に付き合う方法を初心者向けに解説。
一言結論
nullとは「参照先が存在しない」状態。参照型変数はメモリアドレスを持つが、nullはアドレスがない状態であり、そこにメソッドを呼ぶとNPEになる。「値がない」の表現としてOptionalが設計された。
nullを発明した人の後悔
nullを最初に発明したのはトニー・ホア(Tony Hoare)という計算機科学者だ。1965年のこと。
彼は後にこう語っている:
「私の10億ドルの失敗(My Billion-Dollar Mistake)」
null参照を発明したことで、以来何十年もの間、数え切れないほどのバグ、脆弱性、システム障害が引き起こされてきた、と。
なぜそこまで言われるのか。nullの仕組みを理解しよう。
nullとは「参照先がない」状態だ
前の記事(==とequals()の違い)で説明したように、参照型変数は**メモリアドレス(オブジェクトがある場所)**を格納している:
String s = "hello";
変数s: [ 0x100 ] ← 「0x100番地を見ろ」
↓
[ "hello" ] ← 実体はここ
nullは「アドレスが存在しない」状態だ:
String s = null;
変数s: [ null ] ← 「どこも指していない」
「どこも指していない」変数に対してメソッドを呼び出そうとすると:
String s = null;
int len = s.length(); // ← 「s が指す場所にある length() を呼ぶ」 → 指す場所がない!
JVMは「どこを探せばいいのか」わからない。これが NullPointerException(NPE) だ。例外名の通り「nullのポインタ(参照)に対してアクセスしようとした」エラーだ。
なぜnullが必要なのか
ではnullをなくせばいいか?なくせない理由がある。
「値がない」という状態をどう表現するかという問題だ。
例えばデータベースで「名前が未登録のユーザー」を表したいとき:
// nullなしではどう表現する?
String name = ""; // 空文字?でも "" と「未登録」は別物だ
String name = "UNKNOWN"; // マジックワード?判定が面倒で間違いやすい
String name = null; // ← これが「値がない」の自然な表現
参照型はオブジェクトを指すか指さないかのどちらかしかない。「指さない」を表現する手段がnullだ。プリミティブ型(intなど)にnullがないのは、値を直接格納するので「指す」「指さない」という概念がそもそもないからだ。
NPEが起きる主なパターン
パターン1:初期化忘れ
String name; // 宣言だけして初期化していない(フィールドはnullになる)
int len = name.length(); // NPE
フィールドは宣言時にデフォルト値が入る(参照型はnull、intは0)。ローカル変数は初期化前に使おうとするとコンパイルエラー。
パターン2:メソッドがnullを返す
String result = findUser("未登録のID"); // ユーザーが見つからない → null を返す
System.out.println(result.toUpperCase()); // NPE
「見つからなかったらnullを返す」というパターンは非常に多い。呼び出し側がnullチェックを忘れると落ちる。
パターン3:配列の要素
String[] names = new String[3]; // 要素はすべてnull
System.out.println(names[0].length()); // NPE
配列の要素は参照型なら全部nullで初期化される。
防衛的プログラミング
古典的な対処法はnullチェックだ:
String result = findUser(id);
if (result != null) {
System.out.println(result.toUpperCase());
} else {
System.out.println("ユーザーが見つかりません");
}
でも「nullを返す可能性があるメソッド」が増えると、至る所にnullチェックが散らばる。コードが汚くなる。しかもチェックを忘れたときにコンパイラは何も言わない。
Java 8のOptional:「nullかもしれない」を型で表現する
Java 8で導入されたOptional<T>は「値がある場合とない場合の両方を型で表現する」コンテナだ:
// nullを返す代わりにOptionalを返す
Optional<String> findUser(String id) {
String user = db.find(id);
return Optional.ofNullable(user); // nullをOptional.empty()に変換
}
// 呼び出し側
Optional<String> result = findUser(id);
// 値があるか確認してから使う
result.ifPresent(user -> System.out.println(user.toUpperCase()));
// または orElse でデフォルト値
String user = result.orElse("ゲスト");
// または例外を投げる
String user = result.orElseThrow(() -> new UserNotFoundException(id));
Optionalを使うと「このメソッドはnullを返すかもしれない」が型で明示される。Stringなら「ある」、Optional<String>なら「あるかもしれない」と型を見るだけでわかる。
String → 必ずある(nullが来たらバグ)
Optional<String> → ない場合もある(必ずないケースを考慮せよ)
ただしOptionalは万能ではない。フィールドやメソッド引数で使うのは設計的に避けるのが一般的で、「メソッドの戻り値」として使うのが主な用途だ。
Java 14以降のNPEメッセージ改善
Java 14以降、NPEのメッセージが大幅に改善された:
String name = user.getAddress().getCity().toUpperCase();
// Java 13以前
NullPointerException
// Java 14以降
NullPointerException: Cannot invoke "String.toUpperCase()" because
the return value of "Address.getCity()" is null
どのメソッドがnullを返したかが一目瞭然になった。デバッグが格段に楽になっている。
NPEに出会ったときの読み方
Exception in thread "main" java.lang.NullPointerException
at Main.processOrder(Main.java:42)
at Main.main(Main.java:10)
スタックトレースを下から読む:
main()がprocessOrder()を呼んだ(Main.java:10)processOrder()の42行目でNPEが発生した
42行目を見て、どの変数がnullの可能性があるかを探す。複数の参照をつなげているなら一つずつ分解して確認する。
// 複合式でNPEが出た場合
String city = order.getCustomer().getAddress().getCity(); // どこがnull?
// 分解して確認
Customer customer = order.getCustomer();
System.out.println("customer: " + customer); // nullか?
Address address = customer.getAddress();
System.out.println("address: " + address); // nullか?
String city = address.getCity();
まとめ
nullとは = 参照先が存在しない状態(アドレスがない)
NPEの原因 = null参照に対してメソッドを呼ぶ(どこも指していない場所へのアクセス)
なぜnullがあるか = 「値がない」を参照型で表現する手段がほかにないから
プリミティブにnullなし = 値を直接格納するので「指す」「指さない」の概念がない
Optional = 「nullかもしれない」を型で表現する(Java 8以降)
対処法 = nullチェック・Optional・nullが返らない設計(null不変クラス)
nullは「存在しない」を表す唯一の手段として不可欠だが、「存在しない可能性を考慮し忘れる」という人間の認知の穴を突く。Optionalやnullオブジェクトパターンなど、nullを「型で見える化」するアプローチが現代では好まれる。それでもnullは消えないので、参照型には「nullが来うる」という前提を持つ習慣が大切だ。