🐡

型で分岐できる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");
}

💡 JDK21でプレビュー導入(JEP 441)、
JDK25で正式対応(JEP 455)


比較まとめ

観点 従来(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チェーンを脱却し、ドメインロジックを構文で表現できるようになった

🔗 参考

GitHubで編集を提案

Discussion