Java switch の進化史|Java 7 から Java 26 まで
Java は 1995 年に登場してから 2025 年でちょうど 30 年を迎えました。本番環境ではまだ Java 6 や Java 8 のプロジェクトも珍しくなく、比較的モダンな現場でも LTS 版である Java 11 や Java 17 に固定されていることがよくあります。現時点での最新 LTS は 2025 年 9 月リリースの Java 25、最新安定版は 2026 年 3 月にリリースされた Java 26 です。その長い年月の間に、地味な switch キーワードも何度も拡張されてきました。String case、switch 式、矢印構文での直接的な戻り値、マルチ定数 case、新しい yield キーワード、そして Java 21 で正式機能となったパターンマッチング。本記事では、それらがどのバージョンで追加されたのかを順に追っていきます。
Java 7
String case を受け取れる switch
Java 7 より前の switch は整数型または char 型しか受け取れませんでした。String を渡すと以下のようなコンパイルエラーが出ます。
test/App.java:27: incompatible types
found : java.lang.String
required: int
switch (url) {
^
1 error
Java 7 からは String も指定できるようになりました。
String str = "thekodelab.com";
switch (str) {
case "thekodelab.com": return "Blog Home";
case "google.com": return "Search Engine";
default: return "UNKNOWN";
}
内部的には、コンパイラは String.hashCode() を呼んで switch の引数を int に変換しています。つまりバイトコードレベルでの比較は依然として整数です。ただしハッシュコードは衝突する可能性があるため、コンパイラは各 case の内部に String.equals(Object) を使った二次チェックを自動挿入します。詳細は How switch case with string (Java 1.7 で導入) は内部でどう動くのか? を参照してください。
ちなみに enum 型を switch に渡した場合も仕組みは同じで、コンパイラは Enum.ordinal() を呼んで整数に変換します。つまり switch の引数はバイトコードレベルでは常に整数として扱われてきました(char も結局は小さな整数型です)。その副作用として、String や enum を switch に渡すときは null であってはいけません。コンパイラが挿入する hashCode() や ordinal() の呼び出しで先に NullPointerException が発生するため、default: にすらたどり着けないのです。これらの制約があるのは、switch がコンパイル時に最適化されるからで、実行時には if/else の連鎖よりも速く動きます。なお Java 21 からは null case が言語仕様として使えるようになりました(後述)。
Java 12 から 14
Java 12 では switch 関連のプレビュー機能がまとめて導入されました。Java 13 で微調整が入り、Java 14 で正式機能として確定しました。プレビューとは「今後変更される、あるいは削除される可能性がある機能」を意味し、正式機能になれば「仕様が固まり、安心して本番で使える」ことを意味します。
- JEP 325: Switch Expressions (Preview) — openjdk.org/jeps/325
- JEP 354: Switch Expressions (Second Preview) — openjdk.org/jeps/354
- JEP 361: Switch Expressions — openjdk.org/jeps/361
以降のコード例では、次の enum を前提とします。
public enum EUrl {
HOME, GOOGLE, BING, OTHER;
}
文(statement)と式(expression)
簡単な整理から始めましょう。文(statement) は変数の宣言、状態の変更、制御フローといった「動作」を表すコードの単位で、それ自体は値を返しません。式(expression) は評価すると値になるコードの単位で、四則演算、論理演算、戻り値を持つメソッド呼び出しなどが該当します。int x = 1 + 2 * 3; という一行は全体としては文(x を束縛する)であり、右辺の 1 + 2 * 3 は 7 に評価される式です。
switch 式
もともと switch は文でした。入力として式を受け取り(switch(num)、switch(1 + 1)、switch(getNumber()) など)、case で分岐して何らかの制御を行うもので、それ自体が値を返すことはありませんでした。case の中で return を使えば戻り値のような挙動は得られますが、あれは単に現在のメソッドを抜ける制御フローであり、同じメソッド内で結果を受け取って処理を続けることはできません。
Java 12 でプレビューされた switch 式は、Java 14 で正式機能となり、switch 自体を値を返す式として扱えるようになりました。
String url = switch (eurl) {
case HOME -> "thekodelab.com";
case GOOGLE -> "google.com";
case BING -> "bing.com";
default -> "UNKNOWN";
};
// セミコロンを忘れずに
新しい case LABEL -> VALUE 構文では、右辺の値が switch 式全体の値として左辺の変数に直接代入されます。従来は case LABEL: return VALUE; のように書くしかなく、それだとメソッド自体を抜けてしまうため、再利用しないロジックでもわざわざ別メソッドに切り出す必要がありました。
Visual Studio Code で Java 14 より古い JDK を使うと、上のコードは Switch Expressions are supported from Java 14 onwards only Java(1073743545) というエラーになります。
新しいキーワード yield と case ブロック
矢印構文は簡潔ですが、case の中で複数の文を実行したい場合もあります。そのときは波括弧で囲んだブロックを使い、戻り値は新しいキーワード yield で返します。先ほどの例に、default でのログ出力と null チェックを追加した例を示します。
String url = switch (eurl) {
case HOME -> "thekodelab.com";
case GOOGLE -> "google.com";
case BING -> "bing.com";
default -> {
System.err.println("Unknown type");
if (defaultUrl != null) {
yield defaultUrl;
} else {
yield "UNKNOWN";
}
}
};
Java 12 の JEP 325 では当初 break VALUE; という構文が使われていましたが、Java 13 の JEP 354 でこの書き方は廃止され、新しいキーワード yield VALUE; に置き換えられました。break の後ろにはラベル名を書くこともでき、紛らわしかったためです。詳しくは Java 13 で導入された新しいキーワード “yield” とは何か? を参照してください。yield は Java 14 で正式機能になり、安心して使えます。
マルチ定数 case
これは 1 つの case で複数の値を受け取れるようにする機能です。たとえば case 1, 2, 3: や case 1, 2, 3 -> のように書けます。Java 12 のプレビューで導入され、Java 14 で正式機能になりました。
String type = switch (eurl) {
case HOME -> "My Blog https://thekodelab.com/";
case GOOGLE, BING -> "Search Engine";
default -> {
System.err.println("Unknown type");
yield "UNKNOWN";
}
};
Java 14 より古い JDK で上のコードを VS Code で書くと Multi-constant case labels supported from Java 14 onwards only Java(1073743543) と表示されます。
一部の記事ではこの機能を「マルチバリュー case」と呼んでいますが、その言い方だと case に変数を書けるかのような誤解を招きます。実際には変数は書けません。変数を許すとコンパイル時の最適化ができなくなるためです。「マルチ定数(multi-constant)」という呼び方のほうが正確です。
Java のように事前コンパイルされる言語で switch が if/else より速いのは、case のラベルがコンパイル時に確定していることが前提だからです。分岐条件が実行時にしか決まらない変数なら、どうしても if/else で書くしかありません。さらにコンパイラは case の値の密度を見て、密集していれば tableswitch、疎であれば lookupswitch という 2 種類のバイトコード命令を使い分けます。密集型のほうが測定可能なレベルで高速です。そしてその結果として、switch の case は整数型の定数でなければならない、という制約が生まれます。これについてはまた別の機会に掘り下げたいと思います。
Java 17 から 20:パターンマッチングの 4 世代プレビュー
Java 17 は LTS 版であり、この世代から switch にパターンマッチングが入り始めました。ただしこの時点ではまだプレビューです。しかも Java 17、18、19、20 の 4 世代連続で、同じ機能が毎回プレビューとして再登場し、その都度細部の構文が調整されてきました。本章で扱うのはすべてそのプレビュー期の挙動で、最終的には Java 21 の JEP 441 で一斉に正式機能へ昇格します。次の章で詳しく見ていきます。
null case(プレビュー)
前述のとおり switch はコンパイル時に整数比較へと最適化されるため、これまで null を受け付けることができませんでした。このプレビュー機能では、case に null を明示的に書けるようになります。本質はシンタックスシュガーで、コンパイラが null チェックを挿入してくれるので、開発者としては他の case と同じように書けるわけです。
EUrl eurl = null;
String type = switch (eurl) {
case null -> {
System.err.println("It's null!");
yield "UNKNOWN";
}
case HOME -> "My Blog https://thekodelab.com/";
case GOOGLE, BING -> "Search Engine";
default -> {
System.err.println("Unknown type");
yield "UNKNOWN";
}
};
パターンマッチング(プレビュー)
この機能を理解するには、まず Java 16 で正式機能となった JEP 394: Pattern Matching for instanceof を押さえておくとわかりやすいです。型チェックと型付きの変数束縛を 1 つの式で書けるようになる機能です。
// 従来のコード
if (obj instanceof Integer) {
Integer num = (Integer) obj;
System.out.printf("%d", num);
} else if (obj instanceof Double) {
Double num = (Double) obj;
System.out.printf("%f", num);
}
// Java 16 のパターンマッチング
if (obj instanceof Integer num) {
System.out.printf("%d", num);
} else if (obj instanceof Double num) {
System.out.printf("%f", num);
}
新しい書き方のほうが明らかに短くなっているので、switch にも同じ仕組みを導入しよう、というのがこのプレビューの狙いです。4 世代にわたってプレビューされてきた書き方は、プレビュー期間中こんな形をしていました。
String str = switch (obj) {
case Integer n -> String.format("int %d", n);
case Double n -> String.format("double %f", n);
default -> obj.toString();
};
実行時の型によって処理を分岐したいコードを書いたことがある人なら、instanceof の連鎖より遥かに読みやすくなることが実感できると思います。
新しいキーワード when(プレビュー)
こちらもシンタックスシュガーです。case でマッチした後にさらに if で絞り込みたい場合、素直に書くとネストが深くなりがちです。新しい when キーワードを使うと、case ラベルに直接ガード条件を添えられるようになります。
// `when` を使わない場合
String str = switch (obj) {
case Integer n -> {
if (n >= 100) {
yield String.format("int %06d", n);
} else {
yield String.format("int %03d", n);
}
}
default -> obj.toString();
};
// `when` を使う場合
String str = switch (obj) {
case Integer n when n >= 100 -> String.format("int %06d", n);
case Integer n -> String.format("int %03d", n);
default -> obj.toString();
};
少し惜しいのは、同じ型に対して複数の when を書きたいときは Integer n を毎回繰り返す必要がある点です。この制約は Java 21 の正式版でも同じ設計のまま維持されています。
プレビュー 4 世代の JEP 一覧
詳細を追いたい方は OpenJDK の JEP ページが一次ソースです。Java 17 から Java 20 まで、各世代ごとの switch パターンマッチングのプレビュー JEP をまとめておきます。
- Java 17 — JEP 406: Pattern Matching for switch (Preview) — openjdk.org/jeps/406
- Java 18 — JEP 420: Pattern Matching for switch (Second Preview) — openjdk.org/jeps/420
- Java 19 — JEP 427: Pattern Matching for switch (Third Preview) — openjdk.org/jeps/427
- Java 20 — JEP 433: Pattern Matching for switch (Fourth Preview) — openjdk.org/jeps/433
Java 21:パターンマッチングの正式登場
Java 21 は 2023 年 9 月にリリースされた、Java 17 に続く LTS 版で、switch の歴史上もっとも重要なマイルストーンのひとつです。Java 17 から Java 20 まで 4 世代連続でプレビューされてきたパターンマッチング for switch が、この版で JEP 441: Pattern Matching for switch として一斉に正式機能へ昇格しました。null case、型パターン、when ガードまで含めて、本番プロジェクトのコードに安心して書き込めます。
Java 21 ではさらに、sealed インターフェースと enum に対する網羅性チェック(exhaustiveness check)が導入されました。つまり switch のセレクタが sealed インターフェースである場合、コンパイラが許可されたサブタイプすべてに対応する case があるかをチェックし、抜けがあればコンパイルが通りません。これにより switch は型安全な dispatch 機構として正式に使えるようになったわけです。
以下は null case、型パターン、when ガード、sealed の網羅性を全部組み合わせた例です。
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}
static double area(Shape shape) {
return switch (shape) {
case null -> 0;
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s when s.side() > 0 -> s.side() * s.side();
case Square s -> 0;
case Triangle t -> t.base() * t.height() / 2;
};
}
Java 21 の正式版では、null と default を case null, default -> としてまとめて書くこともできます。null に特別な処理が必要ないなら、こちらのほうがすっきり書けます。sealed の網羅性チェックは、将来 Shape に新しいサブタイプが追加されたとき、対応できていないすべての switch がコンパイル時点で検出されることを意味し、「新しい型の処理を入れ忘れた」系のバグをかなり減らしてくれます。
Java 23 から 26:Primitive Types in Patterns(依然プレビュー)
Java 21 以降、switch の基本構文自体はほぼ落ち着きましたが、パターンマッチングのエコシステムはまだ拡張中です。Java 23 からは OpenJDK が switch まわりの新しいプレビュー機能として Primitive Types in Patterns, instanceof, and switch を投入し始めました。この機能では case ラベルでプリミティブ型のパターンマッチングが直接できるようになり、さらに switch のセレクタ式も long、float、double、boolean が書けるようになります。もう整数系の型に制限されていません。
この機能は Java 23 で初めてプレビューされて以来、Java 24、Java 25 LTS、Java 26 と継続してプレビュー状態のままで、Java 26 時点で 4 回目のプレビューです。定版時期はまだ未定です。書き方は次のような感じです。
// switch のセレクタは long/float/double/boolean が使える
long code = 42L;
String size = switch (code) {
case 0L -> "zero";
case 1L, 2L, 3L -> "small";
default -> "other";
};
// case にプリミティブ型のパターンを書き、
// `when` ガードや自動的な型変換ルールと組み合わせられる
Object obj = 42;
String label = switch (obj) {
case int i when i >= 0 -> "non-negative int " + i;
case int i -> "negative int " + i;
case long l -> "long " + l;
case double d -> "double " + d;
default -> "other";
};
このプレビューの肝は「unconditional exactness(無条件の正確性)」という概念の拡張です。2 つの型の間での変換が情報を失わないなら(例:int は double に無条件に広げられるが、逆だと精度を失うことがある)、コンパイラは暗黙的にパターンマッチングを許可し、そうでなければ開発者に意図を明示するよう要求します。プレビューを重ねるごとにこのルールが細かく調整され、switch の挙動が言語の他の部分と整合するよう詰められてきました。Java 26 の JEP 530 では dominance チェック(支配関係チェック)もさらに締められています。
- Java 23 — JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) — openjdk.org/jeps/455
- Java 24 — JEP 488 (Second Preview) — openjdk.org/jeps/488
- Java 25 — JEP 507 (Third Preview) — openjdk.org/jeps/507
- Java 26 — JEP 530 (Fourth Preview) — openjdk.org/jeps/530
まとめ
Java 7 の String case、Java 14 の switch 式と yield、Java 21 の正式版パターンマッチング、そして Java 26 でもなお進化し続ける Primitive Types in Patterns まで、switch キーワードは素朴な条件分岐から、モダン Java の中心に据えられた型安全で可読性の高い dispatch 機構へと変貌してきました。もしまだ Java 8 や Java 11 に留まっているなら、Java 21 や現行 LTS の Java 25 に上げるだけで、switch をよりシンプルかつ安全に書き直せるようになります。週末を使って、古いプロジェクトに残った switch/case のブロックをリファクタリングしてみるだけの価値は十分にあります。