なぜ「null と switch」をちゃんと考えないといけないのか
switch は「値に応じて分岐する」構文ですが、
その値が null になりうるかどうかを意識していないと、
実行時にいきなり NullPointerException(NPE)が飛ぶポイントになります。
昔の switch は「null を絶対に許さない」仕様でしたが、
モダンな switch(switch 式・パターンマッチング付き switch)では、null を 明示的に扱う ことができるようになってきています。
ここでは、
- 旧来の
switchとnullの関係 - モダンな
switchでのnullの扱い - 設計として「null をどう位置づけるか」
を、初心者向けにかみ砕いて整理していきます。
旧来の switch:null を渡した瞬間に NPE
古い switch 文の挙動
まず、昔ながらの switch 文から。
String s = null;
switch (s) {
case "A":
System.out.println("A");
break;
default:
System.out.println("other");
}
Javaこれを実行すると、default に行くのではなく、switch (s) の時点で NullPointerException が発生します。
理由はシンプルで、
旧来の switch は「null を値として扱う」ことを想定していないからです。
つまり、
switchに渡す値がnullになりうるなら、
事前にif (s == null)でチェックしておかないと危険
というのが、昔からの鉄則でした。
switch 式(Java 14+)でも基本は同じ:null はそのままでは NPE
switch 式も「素のままでは null 非対応」
モダンな switch 式でも、基本は同じです。
String s = null;
String label = switch (s) {
case "A" -> "エース";
case "B" -> "ビー";
default -> "その他";
};
Javaこれも、default に行く前に NullPointerException になります。
switch 式になっても、
「null を勝手に default に流してくれる」ような甘い仕様にはなっていません。
だからこそ「null をどう扱うか」を設計で決める必要がある
ここで大事なのは、
- 「この
switchに渡す値は、そもそもnullを許すのか?」 - 「許すなら、
nullのときはどういう扱いにするのか?」
を、コードの外側(設計)でちゃんと決めることです。
「とりあえず switch に投げて、default でなんとかする」
という発想だと、null が来た瞬間に落ちます。
パターンマッチング付き switch と null
パターンマッチング付き switch のイメージ
パターンマッチング付き switch(Java 21 以降の機能)では、switch の中で「型」や「条件」に応じて分岐できます。
static String describe(Object obj) {
return switch (obj) {
case String s -> "String: " + s;
case Integer i -> "Integer: " + i;
default -> "other";
};
}
Javaここでも、obj が null なら、
やはり switch (obj) の時点で NullPointerException です。
「null というパターン」を明示的に書けるようになっていく
将来の仕様・プレビュー機能では、case null のように「null そのもの」をパターンとして扱う方向に進んでいます。
イメージとしては、こういう書き方です。
String result = switch (obj) {
case null -> "null でした";
case String s -> "String: " + s;
case Integer i -> "Integer: " + i;
default -> "other";
};
Javaこうなると、
nullを「特別なケース」として明示的に扱えるnullを見落として NPE になる、という事故を減らせる
という方向に進化していきます。
ただし、ここでのポイントは、
「null を勝手に安全にしてくれる」のではなく、
「null をパターンとして“明示的に扱え”と言っている」
ということです。
実務での基本スタンス:「switch に null を渡さない」設計
そもそも null を許さないようにする
一番シンプルで堅い方針は、
「switch に渡す値は、そもそも null にならないようにする」
です。
例えば、メソッドの引数であれば、
void handleStatus(Status status) {
Objects.requireNonNull(status, "status must not be null");
switch (status) {
case OK -> ...
case ERROR -> ...
}
}
Javaのように、入り口で null をはじく。
あるいは、Optional を使って、
「null ではなく Optional.empty() で表現する」
という設計に寄せるのも有効です。
Optional<String> opt = ...;
String label = opt
.map(s -> switch (s) {
case "A" -> "エース";
case "B" -> "ビー";
default -> "その他";
})
.orElse("値がありません");
Javaswitch の中には「非 null の世界」だけを持ち込み、null の扱いは Optional の外側で完結させる、という発想です。
どうしても null を許すなら、前段で分岐する
「歴史的事情で null が来る可能性がある」
「外部ライブラリの仕様で null が返ってくる」
といった場合は、switch の前で分岐してしまうのが現実的です。
String s = getMaybeNull();
String label;
if (s == null) {
label = "null のときの扱い";
} else {
label = switch (s) {
case "A" -> "エース";
case "B" -> "ビー";
default -> "その他";
};
}
Java「null を扱う if」と「値に応じて分岐する switch」を分離することで、switch の中を「非 null 前提の世界」にできます。
設計としての一番大事なポイント:「null を“値”として認めるか?」
null を「普通の値」として扱うと、だいたい破綻する
null を「他の値と同列の、ただの 1 つの値」として扱い始めると、
コードのあちこちで
if (x == null)switchの前の null チェック- 想定外の NPE
が増殖していきます。
switch もその一部で、
「null を普通の値として switch に渡す」
という発想自体が、だいたいコードをつらくします。
「この場では null を許さない」と決めてしまう勇気
だからこそ、設計としては、
- 「このメソッドの引数は null 禁止」
- 「このフィールドは必ず値が入る」
- 「null の可能性があるのは、この層まで」
といった “null の侵入範囲” を決めておくことが大事です。
switch は「値に応じて分岐する」構文なので、
その値が「ちゃんと存在する」前提で使う方が、
読みやすくて安全なコードになります。
まとめ:null と switch を自分の言葉で整理するなら
あなたの言葉でまとめると、こうなります。
「旧来の switch もモダンな switch 式も、switch (x) に null を渡すと NullPointerException になる。
モダンなパターンマッチング付き switch では、case null のように null を明示的に扱う方向に進化しているが、
それは“勝手に安全になる”のではなく、“null をちゃんとパターンとして扱え”というメッセージ。
実務では、
・そもそも switch に渡す値は null を許さない設計にする
・どうしても null が来るなら、switch の前段で if で分岐する
・必要なら Optional で「値がない」ことを表現し、switch の中は非 null の世界にする
という方針で考えると、switch と null の組み合わせで悩むことがかなり減る。」
