🐡
型で分岐できるswitch構文 ─ sealed・recordと組み合わせる実践例 ─
はじめに
Java 25 では、switch文で 型ごとの分岐(Pattern Matching for switch) が正式にサポートされました。
これにより、これまでのように instanceof と明示的キャストを組み合わせていたコードを、
1つの switch 文で安全かつシンプルに記述できるようになりました。
本記事では、この機能の概要と、実務での具体的な活用例を整理します。
📊 新旧構文の比較
APIから受け取るレスポンスの型(例:Success, Error)に応じてログを出し分けたい場合を考えます
同様の処理をJDK16/JDK21それぞれで記述しています
JDK16以前(従来構文)
if (res instanceof Success) {
Success s = (Success) res;
log.info("Success: {}", s.data());
} else if (res instanceof Error) {
Error e = (Error) res;
log.warn("Error: {}", e.message());
} else {
log.info("unknown");
}
JDK21以降(Pattern Matching for switch)
switch (res) {
case Success s -> log.info("Success: {}", s.data());
case Error e -> log.warn("Error: {}", e.message());
default -> log.info("unknown");
}
比較まとめ
| 観点 | 従来(JDK16以前) | 新構文(JDK21以降) |
|---|---|---|
| 型判定 |
instanceof + 明示的キャストが必要 |
型パターンにマッチすれば自動で変数利用可 |
| 可読性 | if-elseが増えるとネストが深くなる | 各型ごとにシンプルにcase分岐 |
| 安全性 | キャストミスのリスクあり | コンパイラが型整合性をチェック |
| スコープ | すべての変数が同一スコープ | 各caseごとに独立したスコープ |
| null処理 | 明示的チェックが必要 |
case null ->で安全に扱える |
| sealedとの連携 | 手動で全分岐確認が必要 | コンパイル時に網羅性を検証 |
要点まとめ
-
switchひとつで型ごとの安全な分岐処理が可能に -
sealed/recordと組み合わせることで、
網羅性チェック × ドメイン表現力向上を両立 - 実務ではログ出し分け、イベント処理、認証・通知ハンドラなどに有効
🧪 実践
switchによる型ごとの分岐を下記の3パターンについて実際にどのように活用できるかを見ていきます。
- イベント処理
- データ変換
イベント駆動処理での型安全な分岐
KafkaやPulsarなどのメッセージングキューより、複数のイベント型を受信して処理を分けたい場合。
sealed interface DomainEvent permits UserRegistered, OrderPlaced, InventoryUpdated {}
record UserRegistered(String userId, String email) implements DomainEvent {}
record OrderPlaced(String orderId, int totalPrice) implements DomainEvent {}
record InventoryUpdated(String productId, int stockDelta) implements DomainEvent {}
void handle(DomainEvent event) {
switch (event) {
case UserRegistered e ->
log.info("ユーザー登録: {}", e.email());
case OrderPlaced e ->
log.info("注文受付: {} 金額: {}", e.orderId(), e.totalPrice());
case InventoryUpdated e ->
log.info("在庫更新: {} 残数変化: {}", e.productId(), e.stockDelta());
}
}
MQなどで受け取るイベントを switch 一発で型安全に分岐可能
新しいイベント型を追加した際も、未対応の分岐があればコンパイル時に検知できます
データ変換処理
受け取るデータフォーマット(JSON / CSV / XML)を判定してパーサーを呼び分ける場合です
sealed interface DataFormat permits JsonData, CsvData, XmlData {}
record JsonData(String content) implements DataFormat {}
record CsvData(String content) implements DataFormat {}
record XmlData(String content) implements DataFormat {}
static void parse(DataFormat data) {
switch (data) {
case JsonData j -> log.info("JSONを解析: {}", parseJson(j.content()));
case CsvData c -> log.info("CSVを解析: {}", parseCsv(c.content()));
case XmlData x -> log.info("XMLを解析: {}", parseXml(x.content()));
}
}
型パターンを利用することで、入力フォーマットに応じた処理分岐を
安全かつ明示的に記述できます。
「フォーマット判定→パーサ呼び分け」を行う共通モジュールで特に有効。
補足:型分岐が活きる実務ドメイン例一覧
| 分類 | 想定ケース | キーになる型 |
|---|---|---|
| イベント駆動処理 | MQ / PubSub / Webhookイベント | sealed interface Event permits ... |
| ドメインモデリング | 値オブジェクト、サブタイプ階層 |
sealed interface + record
|
| データ変換 | 入出力フォーマットやファイル形式 | sealed interface DataFormat |
| 認証処理 | Basic / Bearer / APIKeyなど方式別ハンドラ | sealed interface AuthScheme |
| 通知処理 | Email / Slack / WebPushなど送信手段 | sealed interface NotificationChannel |
| 決済処理 | クレジット / PayPay / 銀行振込など | sealed interface PaymentMethod |
🧾 まとめ
-
switchで型を扱えるようになり、安全で読みやすい分岐処理が可能に -
sealed/recordと併用することで、網羅性と拡張性の両立を実現 - ifチェーンを脱却し、ドメインロジックを構文で表現できるようになった
Discussion