- JAVA
Java switchパターンマッチング|record・sealedの書き方と実例
Javaのswitchパターンマッチングとは、case Circle c -> のように「型」や「recordの中身の形」で分岐し、同時に変数へ取り出せる構文で、Java 21(LTS)で正式機能になりました。 sealed と組み合わせると、コンパイラが「すべての型を処理しているか」を確認してくれるため、default を書かずに抜け漏れのない分岐が作れます。
この記事では、Java 21.0.10 で実際に動かした完成コードと出力、わざと間違えたときの本物のコンパイルエラー文、従来の if-instanceof からの書き換え対照表、Java 16〜26 のバージョン別機能表、そして「いつ switch パターンを使うか」の判断基準までをまとめています。プレビュー時代(Java 17〜20)の構文ではなく、正式版の書き方だけを扱います。
switchパターンマッチングの早見表
| やりたいこと | 書き方(Java 21) | ポイント |
|---|---|---|
| 型で分岐して変数に受ける | case String s -> ... |
型パターン。キャスト不要 |
| recordの中身を取り出す | case Point(int x, int y) -> ... |
recordパターン(分解) |
| 型を省略して取り出す | case Point(var x, var y) -> ... |
var で型推論 |
| ネストしたrecordを一度に分解 | case Rect(Point(var x1, var y1), Point p2) -> ... |
何段でもネスト可 |
| 条件を追加する | case Integer i when i < 0 -> ... |
when ガード。具体的な case を先に |
| nullも分岐する | case null -> ... |
書かないと NullPointerException |
| nullとそれ以外をまとめる | case null, default -> ... |
最後に1つだけ |
| 抜け漏れをコンパイラに確認させる | 入力型を sealed にする |
default なしで網羅できる |
| 型が閉じていない(Object等) | default -> ... を書く |
書かないと「does not cover all possible input values」 |
| 使わない要素を無視する | case Line(Point(var x, _), _) |
無名パターン _(Java 22 で正式) |
| 文(statement)でも使う | switch (s) { case Circle c -> ...; } |
パターンを使う switch 文も網羅性チェックの対象 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java switchパターンマッチング|record・sealedの書き方と実例」
動作確認した環境
| 項目 | 内容 |
|---|---|
| JDK | OpenJDK 21.0.10(LTS) |
| 実行方法 | java ファイル名.java(単一ファイル実行。javac 不要) |
| 外部ライブラリ | なし(標準のみ) |
| 文字コード | 日本語出力のため -Dstdout.encoding=UTF-8 を付けて実行 |
以下の実行結果はすべてこの環境で実際に出力したものです。
完成コード:型パターン・recordパターン・when・case null・sealed をまとめて動かす
まずは全部入りのコードです。図形(円・長方形・三角形)と、注文の状態(受付・発送・お届け・キャンセル)という架空の題材で、switch パターンの主要機能を1ファイルで確認できます。PatternDemo.java として保存し、そのまま実行できます。
import java.util.List;
public class PatternDemo {
// 1. sealed で「とりうる型」を閉じる
sealed interface Shape permits Circle, Rect, Triangle {}
record Point(int x, int y) {}
record Circle(Point center, int r) implements Shape {}
record Rect(Point topLeft, Point bottomRight) implements Shape {}
record Triangle(Point a, Point b, Point c) implements Shape {}
// 2. sealed + record パターン:default なしで網羅できる
static double area(Shape s) {
return switch (s) {
case Circle(Point c, int r) -> Math.PI * r * r;
case Rect(Point(var x1, var y1), Point(var x2, var y2))
-> Math.abs(x2 - x1) * Math.abs(y2 - y1);
case Triangle(Point a, Point b, Point c)
-> Math.abs((b.x() - a.x()) * (c.y() - a.y())
- (c.x() - a.x()) * (b.y() - a.y())) / 2.0;
};
}
// 3. when ガード:同じ型を条件で分ける(具体的な case を先に書く)
static String describe(Shape s) {
return switch (s) {
case Circle(Point(var x, var y), int r) when x == 0 && y == 0
-> "原点中心の円 r=" + r;
case Circle c -> "円 r=" + c.r();
case Rect(Point(var x1, var y1), Point(var x2, var y2))
when (x2 - x1) == (y2 - y1) -> "正方形 一辺=" + (x2 - x1);
case Rect r -> "長方形";
case Triangle t -> "三角形";
};
}
// 4. 注文の状態(sealed + record)
sealed interface OrderStatus permits Received, Shipped, Delivered, Canceled {}
record Received(String orderId) implements OrderStatus {}
record Shipped(String orderId, String trackingNo) implements OrderStatus {}
record Delivered(String orderId, int days) implements OrderStatus {}
record Canceled(String orderId, String reason) implements OrderStatus {}
static String message(OrderStatus st) {
return switch (st) {
case null -> "状態が未設定です"; // case null
case Received(var id) -> id + ":受付済み";
case Shipped(var id, var no) -> id + ":発送済み(追跡番号 " + no + ")";
case Delivered(var id, var d) when d <= 2 -> id + ":お届け完了(" + d + "日・早い)";
case Delivered(var id, var d) -> id + ":お届け完了(" + d + "日)";
case Canceled(var id, var why) -> id + ":キャンセル(" + why + ")";
};
}
// 5. Object を受ける場合は default が必要(型が閉じていないため)
static String format(Object o) {
return switch (o) {
case null -> "null";
case Integer i when i < 0 -> "負の整数 " + i;
case Integer i -> "整数 " + i;
case String str when str.isBlank() -> "空の文字列";
case String str -> "文字列 \"" + str + "\"(" + str.length() + "文字)";
case int[] arr -> "int配列 長さ" + arr.length;
case List<?> list -> "List 要素数" + list.size();
default -> "その他 " + o.getClass().getSimpleName();
};
}
public static void main(String[] args) {
List<Shape> shapes = List.of(
new Circle(new Point(0, 0), 2),
new Circle(new Point(3, 4), 1),
new Rect(new Point(0, 0), new Point(3, 3)),
new Rect(new Point(1, 1), new Point(5, 3)),
new Triangle(new Point(0, 0), new Point(4, 0), new Point(0, 3)));
for (Shape s : shapes) {
System.out.printf("%s / 面積=%.2f%n", describe(s), area(s));
}
System.out.println("---");
List<OrderStatus> list = new java.util.ArrayList<>(List.of(
new Received("A-001"), new Shipped("A-002", "TN123"),
new Delivered("A-003", 1), new Delivered("A-004", 5),
new Canceled("A-005", "在庫切れ")));
list.add(null);
for (OrderStatus st : list) System.out.println(message(st));
System.out.println("---");
Object[] values = {null, -5, 42, " ", "Java", new int[]{1, 2, 3}, List.of(1, 2), 3.14};
for (Object v : values) System.out.println(format(v));
}
}
java -Dstdout.encoding=UTF-8 PatternDemo.java
実行結果(OpenJDK 21.0.10):
原点中心の円 r=2 / 面積=12.57
円 r=1 / 面積=3.14
正方形 一辺=3 / 面積=9.00
長方形 / 面積=8.00
三角形 / 面積=6.00
---
A-001:受付済み
A-002:発送済み(追跡番号 TN123)
A-003:お届け完了(1日・早い)
A-004:お届け完了(5日)
A-005:キャンセル(在庫切れ)
状態が未設定です
---
null
負の整数 -5
整数 42
空の文字列
文字列 "Java"(4文字)
int配列 長さ3
List 要素数2
その他 Double
コードの読みどころ
areaにはdefaultがありません。ShapeがsealedでCircle・Rect・Triangleの3つに閉じているため、3つの case で「すべて網羅した」とコンパイラが判断します。Rect(Point(var x1, var y1), Point(var x2, var y2))はネストした分解です。 1行で4つの座標を取り出せます。アクセサ(r.topLeft().x())を連ねる必要がありません。whenガード付きの case は、同じ型のガードなし case より前に書きます。 逆にするとコンパイルエラーになります(後述)。messageのcase nullがあるので、nullを渡しても例外にならず「状態が未設定です」と返ります。format(Object)はdefaultが必須です。Objectには無数の型がありうるため、網羅するにはdefaultが必要です。
構文を1つずつ理解する
型パターン:キャストと変数宣言を1回で
case String s -> s.length();
「String 型なら、s という名前で受け取る」という意味です。 従来は instanceof で判定してからキャストしていましたが、その2手間が1つになります。s のスコープはその case の中だけです。
recordパターン:中身を取り出す(分解)
record Point(int x, int y) {}
case Point(int x, int y) -> x + y; // 型を明示
case Point(var x, var y) -> x + y; // var で推論
case Point p -> p.x() + p.y(); // 分解せず受けるだけ
recordパターンは、recordのコンポーネント(フィールド)を宣言順に取り出します。 名前は自由(Point(int a, int b) でも可)ですが、順番と個数はrecordの宣言と一致させます。 recordそのものの使い方は別記事「Java recordの使い方」にまとめているので、ここでは分解の書き方に絞ります。
ネストしたrecordパターン
record Line(Point from, Point to) {}
case Line(Point(var x1, var y1), Point(var x2, var y2)) -> ...
内側のrecordも同じ書き方で分解できます。 「始点のxだけ欲しい」のように、必要な部分だけ取り出せるのが強みです。
when ガード:型に条件を足す
case Integer i when i < 0 -> "負の整数";
case Integer i -> "0以上の整数";
when の後ろには boolean を返す式を書きます。 パターンで取り出した変数(i)をそのまま使えます。プレビュー時代の && によるガード(Java 17〜19 の一部)とは書き方が異なるので、検索で見つけた記事がどの版かを確認するとスムーズです。
case null:nullも分岐に含める
case null -> "未設定";
case null, default -> "未設定またはその他";
パターンを使う switch でも、case null を書かないと null で NullPointerException になります。 これは従来の switch と同じ動きです。null があり得るなら case null を書く、ありえないなら書かずに例外で早めに気づく、と意図に合わせて選びます。
sealed による網羅性チェック
sealed interface Shape permits Circle, Rect, Triangle {}
sealed は「このインターフェースを実装できるのは permits に書いた型だけ」と宣言する機能です。 switch はこの情報を使い、すべての型を case で書けば default 不要と判断します。後から Hexagon を追加すると、処理を書き忘れた switch がすべてコンパイルエラーになるため、修正箇所をコンパイラが教えてくれます。
よくあるコンパイルエラーと直し方(実際のエラー文)
ここからは、わざと間違えたコードを Java 21.0.10 でコンパイルした実際のエラー文です。エラーの英文で検索してたどり着いた方は、該当する行を探してください。
| エラー文(抜粋) | 原因 | 直し方 |
|---|---|---|
the switch expression does not cover all possible input values |
case が足りない | 足りない型の case か default を足す |
the switch statement does not cover all possible input values |
パターンを使う switch 文で case が足りない | 同上(switch 文でも網羅が必要) |
this case label is dominated by a preceding case label |
広い型やガードなし case が先にある | 具体的な case を上に移す |
incompatible types: Integer cannot be converted to String |
入力型と関係のない型の case | 入力型に合う型だけ書く |
illegal fall-through to a pattern |
コロン形式で break を書かずにパターン case へ落ちる | break を書くか、アロー -> 形式にする |
unnamed variables are a preview feature and are disabled by default. |
Java 21 で _ を使った |
Java 22 以降にするか、変数名を付ける |
エラー1:sealed の型を1つ書き忘れた
sealed interface Shape permits Circle, Rect, Triangle {}
// ...
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rect r -> r.w() * r.h();
// Triangle を書き忘れ
};
}
E1.java:7: error: the switch expression does not cover all possible input values
return switch (s) {
^
1 error
error: compilation failed
直し方:case Triangle t -> ... を追加します。 ここで安易に default を足すと、将来型を追加したときにもこのエラーが出なくなります。sealed の型には default を書かず、全型を列挙するのがおすすめです。
エラー2:Object を受けて default がない
static String f(Object o) {
return switch (o) {
case String s -> "文字列";
case Integer i -> "整数";
};
}
E4.java:3: error: the switch expression does not cover all possible input values
return switch (o) {
^
直し方:default -> "その他"; を最後に追加します。 Object のような閉じていない型では default が必要です。
エラー3:ガードだけでは網羅と判断されない
static String f(Integer n) {
return switch (n) {
case Integer i when i > 0 -> "正";
case Integer i when i <= 0 -> "0以下";
};
}
E5.java:3: error: the switch expression does not cover all possible input values
return switch (n) {
^
人間には「すべての整数を網羅している」と読めますが、コンパイラは when の中身を評価しません。 直し方は、最後の case をガードなしの case Integer i -> "0以下"; にすることです。
エラー4:広い型が先にある(支配される case)
case CharSequence cs -> "文字列っぽいもの";
case String s -> "文字列"; // String は CharSequence の一種
E2.java:5: error: this case label is dominated by a preceding case label
case String s -> "文字列";
^
ガードなしの case とガード付きの case の順番でも同じエラーになります。
case Integer i -> "整数";
case Integer i when i > 100 -> "大きい整数";
E9.java:5: error: this case label is dominated by a preceding case label
case Integer i when i > 100 -> "大きい整数";
^
直し方:狭い(具体的な)case を上に、広い case を下に並べます。 switch は上から順に照合するため、下の case に到達しないことをコンパイラが教えてくれています。
エラー5:入力型と関係のない型を書いた
static String f(Integer n) {
return switch (n) {
case String s -> "文字列";
case Integer i -> "整数";
};
}
E3.java:4: error: incompatible types: Integer cannot be converted to String
case String s -> "文字列";
^
Integer が String になることはないため、コンパイラがあり得ない case として指摘します。 入力型のサブタイプか、関係しうる型だけを書きます。
エラー6:コロン形式で fall-through した
switch (o) {
case String s:
System.out.println("文字列");
case Integer i: // break がないので落ちてくる
System.out.println("整数");
break;
default:
System.out.println("その他");
}
E7.java:6: error: illegal fall-through to a pattern
case Integer i:
^
パターン変数 i は Integer に一致したときだけ値が入るので、String の case から落ちてくることはできません。 アロー形式(->)にすると fall-through 自体が起きないので、パターン switch はアロー形式で書くのが読みやすくおすすめです。
実行時:case null がないと NullPointerException
static String f(Shape s) {
return switch (s) {
case Circle c -> "円";
case Rect r -> "長方形";
};
}
// f(null) を呼ぶ
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at E6.f(E6.java:6)
at E6.main(E6.java:12)
これはコンパイルは通り、実行時に発生します。 null を正常な値として扱いたいときは case null -> ... を書きます。
if-instanceof・従来の switch からの書き換え対照表
| 従来の書き方 | switch パターン(Java 21) | 変わること |
|---|---|---|
if (o instanceof String) { String s = (String) o; ... } |
case String s -> ... |
キャストが不要 |
if (o instanceof String s) {...} else if (o instanceof Integer i) {...} |
switch (o) { case String s -> ...; case Integer i -> ...; default -> ...; } |
分岐が一覧で読める |
if (s instanceof Circle c && c.r() > 10) |
case Circle(int r) when r > 10 -> |
条件と分解が1行に |
p.from().x() のようなアクセサの連鎖 |
case Line(Point(var x, var y), Point to) -> |
ネストを一度に分解 |
else { throw new IllegalArgumentException(); }(抜け漏れ対策) |
sealed + 全型の case | 抜け漏れをコンパイル時に検出 |
if (o == null) {...} を先頭に書く |
case null -> |
null も分岐の1つとして書ける |
switch (o.getClass().getSimpleName()) { case "Circle": ... } |
case Circle c -> |
文字列比較が不要、型安全 |
visitor パターン(accept / visit) |
sealed + switch パターン | 処理を1か所にまとめられる |
書き換えの実例(同じ結果になることを確認)
public class Before {
interface Shape {}
record Circle(int r) implements Shape {}
record Rect(int w, int h) implements Shape {}
// 書き換え前:if-instanceof の連鎖(Java 16 以降の instanceof パターン)
static String before(Shape s) {
if (s instanceof Circle c && c.r() > 10) {
return "大きい円";
} else if (s instanceof Circle c) {
return "円 r=" + c.r();
} else if (s instanceof Rect r) {
return "長方形 " + r.w() + "x" + r.h();
}
throw new IllegalArgumentException("未対応: " + s);
}
// 書き換え後:switch パターン(Java 21)
static String after(Shape s) {
return switch (s) {
case Circle(int r) when r > 10 -> "大きい円";
case Circle(int r) -> "円 r=" + r;
case Rect(int w, int h) -> "長方形 " + w + "x" + h;
default -> throw new IllegalArgumentException("未対応: " + s);
};
}
public static void main(String[] args) {
Shape[] list = {new Circle(20), new Circle(3), new Rect(2, 5)};
for (Shape s : list) System.out.println(before(s) + " | " + after(s));
}
}
実行結果(OpenJDK 21.0.10):
大きい円 | 大きい円
円 r=3 | 円 r=3
長方形 2x5 | 長方形 2x5
ここでは Shape が sealed ではないので default が必要です。 sealed interface Shape permits Circle, Rect {} にすれば default を外せ、型を追加したときにコンパイラが知らせてくれる形になります。
バージョン別の機能表(Java 16〜26)
公式の JEP(JDK Enhancement Proposal)で確認した、パターンマッチング関連の正式化・プレビューの時期です。LTS の Java 21 で、switch パターンと record パターンがそろって正式機能になりました。
| JDK | 機能 | JEP | 状態 |
|---|---|---|---|
| 16 | instanceof のパターンマッチング | JEP 394 | 正式 |
| 16 | record | JEP 395 | 正式 |
| 17(LTS) | sealed クラス | JEP 409 | 正式 |
| 17 | switch のパターンマッチング | JEP 406 | プレビュー(1回目) |
| 18〜20 | switch パターン / record パターン | JEP 420・427・433 / 405・432 | プレビュー |
| 21(LTS) | switch のパターンマッチング | JEP 441 | 正式 |
| 21(LTS) | record パターン | JEP 440 | 正式 |
| 21 | 無名パターン・無名変数 _ |
JEP 443 | プレビュー |
| 22 | 無名パターン・無名変数 _ |
JEP 456 | 正式 |
| 23 | プリミティブ型のパターン(instanceof・switch) | JEP 455 | プレビュー(1回目) |
| 24 | 同上 | JEP 488 | プレビュー(2回目) |
| 25(LTS) | 同上 | JEP 507 | プレビュー(3回目) |
| 26 | 同上 | JEP 530 | プレビュー(4回目) |
Java 21 で _ を使うとどうなるか
使わない要素を _ で読み飛ばす書き方は Java 22 で正式になりました。Java 21 で書くと、次のようにプレビュー機能として扱われます(実際の出力)。
record Point(int x, int y) {}
record Line(Point from, Point to) {}
case Line(Point(var x, _), _) -> "始点のx=" + x;
U1.java:6: error: unnamed variables are a preview feature and are disabled by default.
case Line(Point(var x, _), _) -> "始点のx=" + x;
^
(use --enable-preview to enable unnamed variables)
java --enable-preview --source 21 U1.java で実行すると、Note: U1.java uses preview features of Java SE 21. という注記とともに 始点のx=3 と出力されました。Java 21 で本番コードを書くなら、_ の代わりに var unused のように名前を付けておき、22 以降に移行したときに _ へ置き換えると安心です。
プリミティブ型パターンについて
Java 23 以降のプレビューでは、case int i when i > 100 -> のように プリミティブ型でもパターンが書ける ようになります。Java 26 時点でも4回目のプレビュー(JEP 530)で、正式機能ではありません。本記事のコードは Java 21 で動く範囲に限定しています。
使い分けの判断基準
switch パターン・if-instanceof・ポリモーフィズムの選び方
| 状況 | おすすめ | 理由 |
|---|---|---|
| 型が決まった数だけあり、型ごとに処理を分けたい | sealed + switch パターン | 抜け漏れをコンパイラが検出 |
| 判定したい型が1つだけ | if (o instanceof Foo f) |
switch にするほどでもない |
| 型ごとの処理がその型の責務(例:図形が自分の面積を知っている) | 各クラスにメソッドを実装(ポリモーフィズム) | 処理がデータの近くにまとまる |
| 処理の種類が多く、型に処理を増やしたくない(表示・保存・変換など) | sealed + switch パターン | 型を変えずに処理を外側に追加できる |
| 値(定数)で分岐したい | 従来の switch(case "A", "B" ->)/enum の switch |
パターンは不要 |
外部から渡された Object の中身を判定 |
switch パターン + default |
型の一覧が読みやすい |
| JSON・イベントなど「形の違うデータ」を扱う | record + sealed + switch パターン | データ指向で書ける |
目安として、「型の種類は増えにくく、処理の種類が増えやすい」なら switch パターン、「型が増えやすく、処理は決まっている」ならポリモーフィズムが向いています。
default を書くか書かないか
| 入力の型 | default | 理由 |
|---|---|---|
| sealed インターフェース/クラス(全型を case で列挙) | 書かない | 型追加時にコンパイルエラーで気づける |
| sealed だが一部の型だけ特別扱いしたい | 書く | 残りをまとめて処理 |
Object・通常のインターフェース |
必須 | 型が閉じていない |
| enum(全定数を列挙) | 書かない | sealed と同じ理由 |
よくあるつまずきチェックリスト
- [ ] ガード付き case を、同じ型のガードなし case より上に書いた
- [ ] 広い型(
CharSequence・Object)の case を最後のほうに置いた - [ ] null がありうるなら
case nullを書いた - [ ] sealed の switch に安易に
defaultを足していない - [ ] アロー形式(
->)で書いた(fall-through のエラーを避けられる) - [ ] recordパターンの要素の個数・順番がrecordの宣言と一致している
- [ ] Java 21 では
_を使っていない(22 以降で正式)
よくある質問
Q. Java 17 でも switch のパターンマッチングは使えますか?
A. Java 17 ではプレビュー機能のため、--enable-preview が必要で、構文も正式版と一部異なります(ガードの書き方など)。正式な構文で使うなら Java 21 以降が目安です。
Q. sealed を使わないと switch パターンは使えませんか?
A. 使えます。ただし入力型が閉じていないため default が必要になります。sealed にすると default なしで網羅でき、型を追加したときの書き忘れをコンパイラが見つけてくれます。
Q. switch 文(値を返さない switch)でも網羅性チェックはありますか?
A. あります。パターンや case null を使う switch 文も網羅が必要で、足りないと the switch statement does not cover all possible input values というエラーになります(本文で実際に確認しました)。
Q. record パターンで変数名はrecordのフィールド名と同じにする必要がありますか?
A. 必要ありません。照合は宣言の順番で行われるため、名前は自由です。読みやすさのためにフィールド名に合わせるか、var で短く書くのが一般的です。
まとめ
- switch パターンマッチングは Java 21 で正式化し、型・recordの中身で分岐しながら変数に取り出せます
- recordパターンはネストでき、
Rect(Point(var x1, var y1), Point p2)のように一度に分解できます whenガードは同じ型のガードなし case より上に書きます(逆は「dominated」エラー)- sealed なら
defaultなしで網羅でき、型を追加したときの書き忘れをコンパイラが検出します Objectなど閉じていない型ではdefaultが必須、null を扱うならcase nullを書きます- 無名パターン
_は Java 22 で正式、プリミティブ型パターンは Java 26 時点でプレビューです
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java switchパターンマッチング|record・sealedの書き方と実例」
株式会社RJCでは、Javaのパターンマッチングのような新しい言語機能を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。