【プログラミングしてみよう57】Javaのinstanceofパターンマッチングとswitch式を深める

Java

前回はrecordのコンパクトコンストラクタやカスタムメソッドを学びました。

今回は「instanceofパターンマッチング」と「switch式」をさらに深掘りします。 以前基本を学んだ内容ですが、Sealed Classesとの組み合わせや when ガード条件など、より実践的な使い方を解説します。

パターンマッチングとswitch式の復習

以前学んだ内容を振り返ります。

// 従来のswitch文
switch (day) {
    case "月曜日":
        result = "週の始まり";
        break;
    default:
        result = "その他";
}

// switch式(Java 14+): 値を返せる、breakが不要
String result = switch (day) {
    case "月曜日" -> "週の始まり";
    default       -> "その他";
};

今回はさらに「型によるパターンマッチング」「when ガード条件」「Sealed Classesとの組み合わせ」を学びます。

サンプルコード(switch式の基本とアロー構文)

ファイル作成

「Java45」フォルダに SwitchExprTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

public class SwitchExprTest {
    enum Season { SPRING, SUMMER, AUTUMN, WINTER }

    public static void main(String[] args) {
        // switch式で値を返す
        for (Season season : Season.values()) {
            String description = switch (season) {
                case SPRING -> "桜が咲く季節";
                case SUMMER -> "海水浴の季節";
                case AUTUMN -> "紅葉の季節";
                case WINTER -> "雪の季節";
            };
            System.out.println(season + ": " + description);
        }

        // 複数のcaseをまとめる
        System.out.println("\n--- 季節グループ ---");
        for (Season season : Season.values()) {
            String group = switch (season) {
                case SPRING, SUMMER -> "暖かい季節";
                case AUTUMN, WINTER -> "寒い季節";
            };
            System.out.println(season + ": " + group);
        }

        // switch式でブロックを使い、yieldで値を返す
        System.out.println("\n--- 月から季節へ ---");
        for (int month = 1; month <= 12; month++) {
            String season = switch (month) {
                case 3, 4, 5   -> "春";
                case 6, 7, 8   -> "夏";
                case 9, 10, 11 -> "秋";
                case 12, 1, 2  -> "冬";
                default -> {
                    System.out.println("不正な月: " + month);
                    yield "不明";
                }
            };
            System.out.printf("%2d月: %s%n", month, season);
        }
    }
}

コンパイル

javac SwitchExprTest.java

実行

java SwitchExprTest

実行結果は次のようになります。

SPRING: 桜が咲く季節
SUMMER: 海水浴の季節
AUTUMN: 紅葉の季節
WINTER: 雪の季節

--- 季節グループ ---
SPRING: 暖かい季節
SUMMER: 暖かい季節
AUTUMN: 寒い季節
WINTER: 寒い季節

--- 月から季節へ ---
 1月: 冬
 2月: 冬
 3月: 春
 4月: 春
 5月: 春
 6月: 夏
 7月: 夏
 8月: 夏
 9月: 秋
10月: 秋
11月: 秋
12月: 冬

サンプルコード(Sealed Classes + record + switch式)

ファイル作成

「Java45」フォルダに Expr.java、ExprTest.java という名前でファイルを作成します

コード入力

Expr.java に以下のコードを入力します。

// 数式を表すSealed Interface
public sealed interface Expr permits
        Expr.Num, Expr.Add, Expr.Sub, Expr.Mul, Expr.Div {

    record Num(double value) implements Expr {}
    record Add(Expr left, Expr right) implements Expr {}
    record Sub(Expr left, Expr right) implements Expr {}
    record Mul(Expr left, Expr right) implements Expr {}
    record Div(Expr left, Expr right) implements Expr {}
}

ExprTest.java に以下のコードを入力します。

public class ExprTest {
    // switch式で式を評価する
    static double eval(Expr expr) {
        return switch (expr) {
            case Expr.Num n   -> n.value();
            case Expr.Add a   -> eval(a.left()) + eval(a.right());
            case Expr.Sub s   -> eval(s.left()) - eval(s.right());
            case Expr.Mul m   -> eval(m.left()) * eval(m.right());
            case Expr.Div d   -> {
                double right = eval(d.right());
                if (right == 0) throw new ArithmeticException("0で割ることはできません");
                yield eval(d.left()) / right;
            }
        };
    }

    // switch式で式を文字列化する
    static String stringify(Expr expr) {
        return switch (expr) {
            case Expr.Num n -> String.valueOf(n.value());
            case Expr.Add a -> "(" + stringify(a.left()) + " + " + stringify(a.right()) + ")";
            case Expr.Sub s -> "(" + stringify(s.left()) + " - " + stringify(s.right()) + ")";
            case Expr.Mul m -> "(" + stringify(m.left()) + " * " + stringify(m.right()) + ")";
            case Expr.Div d -> "(" + stringify(d.left()) + " / " + stringify(d.right()) + ")";
        };
    }

    public static void main(String[] args) {
        // (3 + 4) * 2
        var expr1 = new Expr.Mul(
            new Expr.Add(new Expr.Num(3), new Expr.Num(4)),
            new Expr.Num(2)
        );

        // 10 / (5 - 3)
        var expr2 = new Expr.Div(
            new Expr.Num(10),
            new Expr.Sub(new Expr.Num(5), new Expr.Num(3))
        );

        System.out.println(stringify(expr1) + " = " + eval(expr1));
        System.out.println(stringify(expr2) + " = " + eval(expr2));

        // whenガード条件のテスト: 0除算
        var expr3 = new Expr.Div(new Expr.Num(10), new Expr.Num(0));
        try {
            eval(expr3);
        } catch (ArithmeticException e) {
            System.out.println("エラー: " + e.getMessage());
        }
    }
}

コンパイル

javac Expr.java ExprTest.java

実行

java ExprTest

実行結果は次のようになります。

((3.0 + 4.0) * 2.0) = 14.0
(10.0 / (5.0 - 3.0)) = 5.0
エラー: 0で割ることはできません

コードの解説

switch式はswitch文と異なり「値を返せる」点が特徴です。-> のアロー構文を使うと break が不要で、各 case から直接値が返ります。case SPRING, SUMMER -> のように複数の値をカンマで並べてまとめることもできます。

ブロックを使う場合は yield で値を返します。break の代わりが yield です。default ブロックの中で yield "不明" とすることで、ブロック内でも値を返せます。

Expr の例は「数式ツリー」と呼ばれるデータ構造です。Num・Add・Sub・Mul・Div をSealed Interfaceとrecordで表現し、switch式のパターンマッチングで再帰的に評価しています。case Expr.Add a -> のように型変数を受け取り、a.left() や a.right() でフィールドにアクセスしています。

Div の処理ではブロックを使い、eval(d.right()) で右辺を先に計算してから0チェックを行い、問題なければ yield で除算結果を返します。Sealed Interfaceを使っているため、すべての case を網羅しているかコンパイラが確認してくれます。

まとめ

switch式はswitch文と異なり値を返せ、-> アロー構文で break が不要になります。複数の case をまとめたり、ブロック内では yield で値を返したりできます。Sealed Classes・record・パターンマッチングを組み合わせることで、型によって処理を分岐させる設計がコンパイラのサポートを受けながらシンプルに書けます。デコンストラクションパターンと when ガード条件を活用することで、より表現力の高いコードが実現できます。

2026/08/05(水) ぱんまる

コメント

タイトルとURLをコピーしました