はじめに

従来のJavaでは、オブジェクトの型を判定して処理を分岐させる際、instanceof演算子と明示的なキャストを組み合わせる冗長なコードが必要でした。また、switch文は限られた型(整数型、String、enum)しか扱えず、複雑な条件分岐にはif-elseチェーンを使わざるを得ませんでした。

パターンマッチングは、これらの問題を解決するためにJavaに段階的に導入された機能です。Java 16でinstanceofの型パターンが正式導入され、Java 21でswitch式でのパターンマッチングとRecord Patternsが正式機能となりました。

この記事では、パターンマッチングの基本概念から、instanceof型パターン、switch式でのパターンマッチング、Record Patterns、ガード付きパターン(when句)、そしてSealed Classesとの連携による網羅性チェックまで、実践的なコード例とともに解説します。

パターンマッチングとは何か

パターンマッチングは、オブジェクトが特定の構造を持つかどうかをテストし、マッチした場合にそのオブジェクトからデータを抽出する機能です。これにより、型チェックとデータ抽出を1つの式で簡潔に表現できます。

パターンマッチングの導入経緯

パターンマッチングは複数のJEP(JDK Enhancement Proposal)を通じて段階的に導入されました。

機能 正式導入バージョン JEP
instanceof型パターン Java 16 JEP 394
switch式でのパターンマッチング Java 21 JEP 441
Record Patterns Java 21 JEP 440

パターンの種類

Java 21時点で利用可能なパターンは以下の通りです。

パターン種別 説明
型パターン(Type Pattern) 型チェックと変数バインディング String s
レコードパターン(Record Pattern) Recordの分解と成分抽出 Point(int x, int y)
ガード付きパターン(Guarded Pattern) パターンに追加条件を付与 String s when s.length() > 5

パターンマッチングの概念図

flowchart TB
    subgraph "従来のJava"
        direction TB
        A1["instanceof + 明示的キャスト"] --> A2["冗長なコード\n型チェックとキャストが分離"]
        B1["switch文"] --> B2["限定的な型のみ\nInteger, String, enum"]
    end
    
    subgraph "パターンマッチング導入後"
        direction TB
        C1["instanceof型パターン"] --> C2["型チェックと変数バインディングを統合"]
        D1["switch式 + パターン"] --> D2["任意の参照型で分岐可能\n網羅性チェック付き"]
    end

instanceofの型パターン

Java 16以降、instanceof演算子で型パターンを使用できるようになりました。これにより、型チェックと変数への代入を1つの式で行えます。

従来のinstanceofとキャスト

従来のJavaでは、instanceofで型をチェックした後、明示的にキャストする必要がありました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Java 15以前の書き方
public class TraditionalInstanceof {
    public static void main(String[] args) {
        Object obj = "Hello, Pattern Matching!";
        
        if (obj instanceof String) {
            String s = (String) obj;  // 明示的なキャストが必要
            System.out.println(s.toUpperCase());
        }
    }
}

この方法には以下の問題があります。

問題点 説明
冗長性 型名を2回(instanceofとキャスト)書く必要がある
エラーの可能性 キャスト先の型を間違える可能性がある
可読性の低下 本質的な処理が埋もれてしまう

型パターンによる改善

Java 16以降では、instanceofに型パターンを指定することで、型チェックと変数バインディングを同時に行えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Java 16以降の書き方
public class TypePatternInstanceof {
    public static void main(String[] args) {
        Object obj = "Hello, Pattern Matching!";
        
        // 型パターン:型チェックと変数バインディングを統合
        if (obj instanceof String s) {
            System.out.println(s.toUpperCase());  // sはString型として使用可能
        }
    }
}

実行環境: Java 16以降

実行結果:

HELLO, PATTERN MATCHING!

パターン変数のスコープ

パターン変数のスコープはフロー依存です。パターンがマッチした場合にのみ変数が有効になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class PatternVariableScope {
    public static void main(String[] args) {
        Object obj = "Hello";
        
        // パターン変数sはif文のブロック内でのみ有効
        if (obj instanceof String s) {
            System.out.println("Length: " + s.length());
        }
        // ここではsは使用不可
        
        // 論理演算子との組み合わせ
        if (obj instanceof String s && s.length() > 3) {
            System.out.println("Long string: " + s);
        }
    }
}

実行結果:

Length: 5
Long string: Hello

否定パターンとスコープ

パターンがマッチしない場合のスコープも考慮されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class NegatedPatternScope {
    public static void process(Object obj) {
        // パターンがマッチしない場合の早期リターン
        if (!(obj instanceof String s)) {
            System.out.println("Not a string");
            return;
        }
        // ここではsが有効(マッチしなければ既にreturnしているため)
        System.out.println("String value: " + s.toUpperCase());
    }
    
    public static void main(String[] args) {
        process("hello");  // String value: HELLO
        process(42);       // Not a string
    }
}

型パターンの実用例

型パターンはequalsメソッドの実装で特に有用です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public record Point(int x, int y) {
    // Recordでは自動生成されるが、カスタマイズする場合
    @Override
    public boolean equals(Object obj) {
        // 型パターンを使用した簡潔な実装
        return obj instanceof Point p
            && this.x == p.x
            && this.y == p.y;
    }
}

switch式でのパターンマッチング

Java 21では、switch式でパターンマッチングが正式にサポートされました。これにより、任意の参照型に対して型ベースの分岐が可能になりました。

従来のif-elseチェーンの問題

従来、複数の型に対する分岐はif-elseチェーンで記述していました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Java 20以前の書き方
public class TraditionalTypeCheck {
    public static String format(Object obj) {
        String result;
        if (obj instanceof Integer i) {
            result = "Integer: " + i;
        } else if (obj instanceof Long l) {
            result = "Long: " + l;
        } else if (obj instanceof Double d) {
            result = "Double: " + d;
        } else if (obj instanceof String s) {
            result = "String: " + s;
        } else {
            result = "Unknown: " + obj;
        }
        return result;
    }
}

この方法の問題点は以下の通りです。

問題点 説明
冗長性 各分岐で同様の構造を繰り返す
網羅性の保証なし すべてのケースを処理したかコンパイラが検証できない
最適化の制限 O(n)の時間計算量で分岐を評価

switch式でのパターンマッチング

Java 21では、switch式で型パターンを使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Java 21以降の書き方
public class PatternSwitch {
    public static String format(Object obj) {
        return switch (obj) {
            case Integer i -> "Integer: " + i;
            case Long l    -> "Long: " + l;
            case Double d  -> "Double: " + d;
            case String s  -> "String: " + s;
            default        -> "Unknown: " + obj;
        };
    }
    
    public static void main(String[] args) {
        System.out.println(format(42));        // Integer: 42
        System.out.println(format(3.14));      // Double: 3.14
        System.out.println(format("Hello"));   // String: Hello
    }
}

実行環境: Java 21以降

実行結果:

Integer: 42
Double: 3.14
String: Hello

nullの扱い

従来のswitchでは、セレクタ式がnullの場合にNullPointerExceptionがスローされていました。Java 21以降では、case nullラベルで明示的に処理できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class NullHandlingSwitch {
    public static String describe(Object obj) {
        return switch (obj) {
            case null         -> "It's null";
            case String s     -> "String: " + s;
            case Integer i    -> "Integer: " + i;
            default           -> "Something else";
        };
    }
    
    public static void main(String[] args) {
        System.out.println(describe(null));      // It's null
        System.out.println(describe("Hello"));   // String: Hello
    }
}

case null, defaultのように、nulldefaultを組み合わせることもできます。

1
2
3
4
5
6
7
8
9
public class NullDefaultSwitch {
    public static String process(String input) {
        return switch (input) {
            case "start" -> "Starting...";
            case "stop"  -> "Stopping...";
            case null, default -> "Unknown command";
        };
    }
}

パターンの優先順位(ドミナンス)

switch式では、パターンの順序が重要です。より具体的なパターンを先に記述し、より一般的なパターンを後に記述する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class PatternDominance {
    public static void demonstrateDominance(Object obj) {
        // 正しい順序:具体的なパターンを先に
        String result = switch (obj) {
            case String s -> "String: " + s;
            case CharSequence cs -> "CharSequence: " + cs.length();
            default -> "Other";
        };
        System.out.println(result);
    }
    
    // コンパイルエラーの例(順序が逆)
    // public static void dominanceError(Object obj) {
    //     String result = switch (obj) {
    //         case CharSequence cs -> "CharSequence";
    //         case String s -> "String";  // エラー:前のパターンに支配されている
    //         default -> "Other";
    //     };
    // }
}

ドミナンスのルールは以下の通りです。

ルール 説明
サブタイプの優先 StringパターンはCharSequenceパターンより先に記述
定数の優先 定数ケース(case 42)は型パターン(case Integer i)より先に記述
ガード付きの優先 ガード付きパターンは同じ型のガードなしパターンより先に記述

Record Patterns

Java 21では、Record Patternsが正式導入されました。これにより、Recordインスタンスを分解(デストラクチャリング)してコンポーネントを直接抽出できます。

Record Patternsの基本

Record Patternsを使用すると、Recordの各コンポーネントをパターン内で直接取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class RecordPatternBasics {
    record Point(int x, int y) {}
    
    public static void main(String[] args) {
        Object obj = new Point(3, 4);
        
        // 従来の型パターン
        if (obj instanceof Point p) {
            int x = p.x();
            int y = p.y();
            System.out.println("x=" + x + ", y=" + y);
        }
        
        // Record Pattern:コンポーネントを直接抽出
        if (obj instanceof Point(int x, int y)) {
            System.out.println("x=" + x + ", y=" + y);
        }
    }
}

実行環境: Java 21以降

実行結果:

x=3, y=4
x=3, y=4

varを使用した型推論

Record Patternsではvarを使用して型推論を行うこともできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class RecordPatternVar {
    record Point(int x, int y) {}
    
    public static void main(String[] args) {
        Point point = new Point(10, 20);
        
        // varを使用した型推論
        if (point instanceof Point(var x, var y)) {
            System.out.println("Sum: " + (x + y));  // x, yはint型と推論される
        }
    }
}

ネストしたRecord Patterns

Record Patternsの真価は、ネストしたRecordを一度に分解できる点にあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class NestedRecordPatterns {
    record Point(int x, int y) {}
    enum Color { RED, GREEN, BLUE }
    record ColoredPoint(Point point, Color color) {}
    record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
    
    public static void main(String[] args) {
        Rectangle rect = new Rectangle(
            new ColoredPoint(new Point(0, 10), Color.RED),
            new ColoredPoint(new Point(20, 0), Color.BLUE)
        );
        
        // ネストしたRecord Patternで深い階層のデータを直接抽出
        if (rect instanceof Rectangle(
                ColoredPoint(Point(var x1, var y1), var c1),
                ColoredPoint(Point(var x2, var y2), var c2))) {
            System.out.println("Upper-left: (" + x1 + ", " + y1 + ") - " + c1);
            System.out.println("Lower-right: (" + x2 + ", " + y2 + ") - " + c2);
        }
    }
}

実行結果:

Upper-left: (0, 10) - RED
Lower-right: (20, 0) - BLUE

switch式でのRecord Patterns

Record Patternsはswitch式でも使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class RecordPatternSwitch {
    sealed interface Shape permits Circle, Rectangle {}
    record Circle(double radius) implements Shape {}
    record Rectangle(double width, double height) implements Shape {}
    
    public static double calculateArea(Shape shape) {
        return switch (shape) {
            case Circle(var r)          -> Math.PI * r * r;
            case Rectangle(var w, var h) -> w * h;
        };
    }
    
    public static void main(String[] args) {
        System.out.println("Circle area: " + calculateArea(new Circle(5)));
        System.out.println("Rectangle area: " + calculateArea(new Rectangle(4, 3)));
    }
}

実行結果:

Circle area: 78.53981633974483
Rectangle area: 12.0

ジェネリクスとRecord Patterns

ジェネリックなRecordでもRecord Patternsを使用できます。型引数は推論されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GenericRecordPattern {
    record Box<T>(T value) {}
    record Pair<A, B>(A first, B second) {}
    
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>("Hello");
        Box<Box<Integer>> nestedBox = new Box<>(new Box<>(42));
        
        // 型引数は推論される
        if (stringBox instanceof Box(var s)) {
            System.out.println("Boxed value: " + s);
        }
        
        // ネストしたジェネリックRecord
        if (nestedBox instanceof Box(Box(var num))) {
            System.out.println("Nested value: " + num);
        }
        
        Pair<String, Integer> pair = new Pair<>("age", 30);
        if (pair instanceof Pair(var key, var value)) {
            System.out.println(key + " = " + value);
        }
    }
}

実行結果:

Boxed value: Hello
Nested value: 42
age = 30

ガード付きパターン(when句)

Java 21では、パターンにガード(追加の条件式)を付与できます。whenキーワードを使用して、パターンマッチ後に追加の条件をチェックできます。

ガード付きパターンの基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class GuardedPatterns {
    public static String categorize(Object obj) {
        return switch (obj) {
            case String s when s.isEmpty()      -> "Empty string";
            case String s when s.length() > 10  -> "Long string: " + s;
            case String s                        -> "String: " + s;
            case Integer i when i < 0           -> "Negative: " + i;
            case Integer i when i == 0          -> "Zero";
            case Integer i                       -> "Positive: " + i;
            default                              -> "Unknown";
        };
    }
    
    public static void main(String[] args) {
        System.out.println(categorize(""));                    // Empty string
        System.out.println(categorize("Hello World!"));        // Long string: Hello World!
        System.out.println(categorize("Hi"));                  // String: Hi
        System.out.println(categorize(-5));                    // Negative: -5
        System.out.println(categorize(0));                     // Zero
        System.out.println(categorize(42));                    // Positive: 42
    }
}

実行環境: Java 21以降

実行結果:

Empty string
Long string: Hello World!
String: Hi
Negative: -5
Zero
Positive: 42

Record Patternsとガードの組み合わせ

Record Patternsとガードを組み合わせることで、複雑な条件分岐を簡潔に表現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class RecordPatternWithGuard {
    record Point(int x, int y) {}
    
    public static String classifyPoint(Point point) {
        return switch (point) {
            case Point(var x, var y) when x == 0 && y == 0 -> "Origin";
            case Point(var x, var y) when x == 0           -> "On Y-axis";
            case Point(var x, var y) when y == 0           -> "On X-axis";
            case Point(var x, var y) when x == y           -> "On diagonal (x=y)";
            case Point(var x, var y) when x > 0 && y > 0   -> "First quadrant";
            case Point(var x, var y) when x < 0 && y > 0   -> "Second quadrant";
            case Point(var x, var y) when x < 0 && y < 0   -> "Third quadrant";
            case Point(var x, var y)                        -> "Fourth quadrant";
        };
    }
    
    public static void main(String[] args) {
        System.out.println(classifyPoint(new Point(0, 0)));   // Origin
        System.out.println(classifyPoint(new Point(0, 5)));   // On Y-axis
        System.out.println(classifyPoint(new Point(3, 3)));   // On diagonal (x=y)
        System.out.println(classifyPoint(new Point(5, 10)));  // First quadrant
        System.out.println(classifyPoint(new Point(5, -3)));  // Fourth quadrant
    }
}

実行結果:

Origin
On Y-axis
On diagonal (x=y)
First quadrant
Fourth quadrant

ガードの評価順序

ガードは上から順に評価されます。最初にマッチしたパターンとガードの組み合わせが選択されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class GuardEvaluationOrder {
    record Score(int value) {}
    
    public static String grade(Score score) {
        return switch (score) {
            case Score(var v) when v >= 90 -> "A";
            case Score(var v) when v >= 80 -> "B";
            case Score(var v) when v >= 70 -> "C";
            case Score(var v) when v >= 60 -> "D";
            case Score(var v)               -> "F";  // 60未満
        };
    }
    
    public static void main(String[] args) {
        System.out.println(grade(new Score(95)));  // A
        System.out.println(grade(new Score(85)));  // B
        System.out.println(grade(new Score(55)));  // F
    }
}

Sealed Classesと網羅性チェック

パターンマッチングとSealed Classesを組み合わせることで、コンパイラによる網羅性チェック(exhaustiveness checking) が可能になります。

網羅性チェックとは

switch式は必ず値を返す必要があるため、すべてのケースを網羅する必要があります。Sealed Classesを使用すると、コンパイラが許可されたサブタイプをすべて把握しているため、default句なしで網羅性を保証できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ExhaustivenessCheck {
    sealed interface Shape permits Circle, Rectangle, Triangle {}
    
    record Circle(double radius) implements Shape {}
    record Rectangle(double width, double height) implements Shape {}
    record Triangle(double base, double height) implements Shape {}
    
    // defaultなしで網羅性が保証される
    public static double calculateArea(Shape shape) {
        return switch (shape) {
            case Circle(var r)           -> Math.PI * r * r;
            case Rectangle(var w, var h) -> w * h;
            case Triangle(var b, var h)  -> 0.5 * b * h;
            // default不要:すべてのサブタイプをカバー
        };
    }
    
    public static void main(String[] args) {
        System.out.println(calculateArea(new Circle(5)));
        System.out.println(calculateArea(new Rectangle(4, 3)));
        System.out.println(calculateArea(new Triangle(6, 4)));
    }
}

実行環境: Java 21以降

実行結果:

78.53981633974483
12.0
12.0

新しいサブタイプ追加時のコンパイルエラー

Sealed Classesに新しいサブタイプを追加すると、既存のswitch式でコンパイルエラーが発生します。これにより、変更漏れを防ぐことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class ExhaustivenessNewType {
    sealed interface PaymentMethod permits CreditCard, BankTransfer, Cash {}
    
    record CreditCard(String number, String expiry) implements PaymentMethod {}
    record BankTransfer(String accountNumber) implements PaymentMethod {}
    record Cash() implements PaymentMethod {}
    
    public static String processPayment(PaymentMethod method) {
        return switch (method) {
            case CreditCard(var num, var exp) -> "Processing credit card: " + num;
            case BankTransfer(var acc)        -> "Processing bank transfer: " + acc;
            case Cash()                        -> "Processing cash payment";
            // PaymentWallet を追加するとここでコンパイルエラー
        };
    }
}

網羅性チェックのメリット

網羅性チェックには以下のメリットがあります。

メリット 説明
コンパイル時の安全性 未処理のケースがあるとコンパイルエラー
リファクタリング安全性 新しいサブタイプ追加時に影響箇所を自動検出
default句の回避 予期しない値を隠蔽するdefaultが不要
ドキュメント効果 許可されたサブタイプが明示的に宣言される

網羅性チェックの仕組み

flowchart TB
    subgraph "Sealed Interface定義"
        A["sealed interface Shape\npermits Circle, Rectangle, Triangle"]
    end
    
    subgraph "switch式"
        B["switch (shape)"]
        C["case Circle"]
        D["case Rectangle"]  
        E["case Triangle"]
    end
    
    subgraph "コンパイラチェック"
        F{"すべてのpermitsを\nカバー?"}
        G["コンパイル成功"]
        H["コンパイルエラー\n未処理のケースあり"]
    end
    
    A --> B
    B --> C
    B --> D
    B --> E
    C --> F
    D --> F
    E --> F
    F -->|Yes| G
    F -->|No| H

実践的なユースケース

ここでは、パターンマッチングを活用した実践的なユースケースを紹介します。

ユースケース1: JSONライクなデータ構造の処理

異なる型の値を持つJSONライクなデータ構造を型安全に処理できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class JsonValueExample {
    sealed interface JsonValue permits JsonString, JsonNumber, JsonBoolean, JsonArray, JsonObject, JsonNull {}
    
    record JsonString(String value) implements JsonValue {}
    record JsonNumber(double value) implements JsonValue {}
    record JsonBoolean(boolean value) implements JsonValue {}
    record JsonArray(java.util.List<JsonValue> elements) implements JsonValue {}
    record JsonObject(java.util.Map<String, JsonValue> properties) implements JsonValue {}
    record JsonNull() implements JsonValue {}
    
    public static String stringify(JsonValue value) {
        return switch (value) {
            case JsonString(var s)   -> "\"" + s + "\"";
            case JsonNumber(var n)   -> String.valueOf(n);
            case JsonBoolean(var b)  -> String.valueOf(b);
            case JsonNull()          -> "null";
            case JsonArray(var elements) -> {
                var sb = new StringBuilder("[");
                for (int i = 0; i < elements.size(); i++) {
                    if (i > 0) sb.append(", ");
                    sb.append(stringify(elements.get(i)));
                }
                sb.append("]");
                yield sb.toString();
            }
            case JsonObject(var props) -> {
                var sb = new StringBuilder("{");
                var first = true;
                for (var entry : props.entrySet()) {
                    if (!first) sb.append(", ");
                    first = false;
                    sb.append("\"").append(entry.getKey()).append("\": ");
                    sb.append(stringify(entry.getValue()));
                }
                sb.append("}");
                yield sb.toString();
            }
        };
    }
    
    public static void main(String[] args) {
        JsonValue user = new JsonObject(java.util.Map.of(
            "name", new JsonString("Alice"),
            "age", new JsonNumber(30),
            "active", new JsonBoolean(true)
        ));
        System.out.println(stringify(user));
    }
}

実行結果:

{"name": "Alice", "active": true, "age": 30.0}

ユースケース2: Result型によるエラーハンドリング

関数型プログラミングで一般的なResult型をパターンマッチングで処理できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ResultPatternExample {
    sealed interface Result<T> permits Success, Failure {}
    
    record Success<T>(T value) implements Result<T> {}
    record Failure<T>(String error) implements Result<T> {}
    
    public static Result<Integer> divide(int a, int b) {
        if (b == 0) {
            return new Failure<>("Division by zero");
        }
        return new Success<>(a / b);
    }
    
    public static void main(String[] args) {
        // 成功ケース
        Result<Integer> result1 = divide(10, 2);
        String message1 = switch (result1) {
            case Success(var value) -> "Result: " + value;
            case Failure(var error) -> "Error: " + error;
        };
        System.out.println(message1);
        
        // 失敗ケース
        Result<Integer> result2 = divide(10, 0);
        String message2 = switch (result2) {
            case Success(var value) -> "Result: " + value;
            case Failure(var error) -> "Error: " + error;
        };
        System.out.println(message2);
    }
}

実行結果:

Result: 5
Error: Division by zero

ユースケース3: 式評価器(Expression Evaluator)

抽象構文木(AST)をパターンマッチングで評価できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ExpressionEvaluator {
    sealed interface Expr permits Num, Add, Mul, Neg {}
    
    record Num(int value) implements Expr {}
    record Add(Expr left, Expr right) implements Expr {}
    record Mul(Expr left, Expr right) implements Expr {}
    record Neg(Expr expr) implements Expr {}
    
    public static int evaluate(Expr expr) {
        return switch (expr) {
            case Num(var n)           -> n;
            case Add(var l, var r)    -> evaluate(l) + evaluate(r);
            case Mul(var l, var r)    -> evaluate(l) * evaluate(r);
            case Neg(var e)           -> -evaluate(e);
        };
    }
    
    public static String prettyPrint(Expr expr) {
        return switch (expr) {
            case Num(var n)           -> String.valueOf(n);
            case Add(var l, var r)    -> "(" + prettyPrint(l) + " + " + prettyPrint(r) + ")";
            case Mul(var l, var r)    -> "(" + prettyPrint(l) + " * " + prettyPrint(r) + ")";
            case Neg(var e)           -> "-" + prettyPrint(e);
        };
    }
    
    public static void main(String[] args) {
        // (3 + 4) * 2 = 14
        Expr expr = new Mul(
            new Add(new Num(3), new Num(4)),
            new Num(2)
        );
        
        System.out.println("Expression: " + prettyPrint(expr));
        System.out.println("Result: " + evaluate(expr));
        
        // -(5 + 3) = -8
        Expr expr2 = new Neg(new Add(new Num(5), new Num(3)));
        System.out.println("Expression: " + prettyPrint(expr2));
        System.out.println("Result: " + evaluate(expr2));
    }
}

実行結果:

Expression: ((3 + 4) * 2)
Result: 14
Expression: -(5 + 3)
Result: -8

ユースケース4: イベント処理システム

異なる種類のイベントを型安全に処理できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class EventHandlerExample {
    sealed interface Event permits UserEvent, SystemEvent {}
    
    sealed interface UserEvent extends Event permits LoginEvent, LogoutEvent, ActionEvent {}
    sealed interface SystemEvent extends Event permits StartupEvent, ShutdownEvent, ErrorEvent {}
    
    record LoginEvent(String userId, java.time.Instant timestamp) implements UserEvent {}
    record LogoutEvent(String userId, java.time.Instant timestamp) implements UserEvent {}
    record ActionEvent(String userId, String action, java.util.Map<String, Object> data) implements UserEvent {}
    
    record StartupEvent(String serviceName) implements SystemEvent {}
    record ShutdownEvent(String serviceName, int exitCode) implements SystemEvent {}
    record ErrorEvent(String message, Throwable cause) implements SystemEvent {}
    
    public static void handleEvent(Event event) {
        switch (event) {
            case LoginEvent(var userId, var time) -> 
                System.out.println("User " + userId + " logged in at " + time);
                
            case LogoutEvent(var userId, var time) -> 
                System.out.println("User " + userId + " logged out at " + time);
                
            case ActionEvent(var userId, var action, var data) -> 
                System.out.println("User " + userId + " performed: " + action);
                
            case StartupEvent(var service) -> 
                System.out.println("Service started: " + service);
                
            case ShutdownEvent(var service, var code) when code == 0 -> 
                System.out.println("Service " + service + " shutdown normally");
                
            case ShutdownEvent(var service, var code) -> 
                System.out.println("Service " + service + " shutdown with code: " + code);
                
            case ErrorEvent(var msg, var cause) -> 
                System.out.println("Error: " + msg + " - " + cause.getClass().getSimpleName());
        }
    }
    
    public static void main(String[] args) {
        handleEvent(new LoginEvent("user123", java.time.Instant.now()));
        handleEvent(new StartupEvent("api-server"));
        handleEvent(new ShutdownEvent("api-server", 0));
        handleEvent(new ShutdownEvent("worker", 1));
        handleEvent(new ErrorEvent("Connection failed", new java.io.IOException("Timeout")));
    }
}

実行結果:

User user123 logged in at 2026-01-03T03:00:00Z
Service started: api-server
Service api-server shutdown normally
Service worker shutdown with code: 1
Error: Connection failed - IOException

まとめ

この記事では、Javaのパターンマッチング機能について解説しました。

機能 導入バージョン 主なメリット
instanceof型パターン Java 16 型チェックとキャストの統合
switch式でのパターンマッチング Java 21 任意の参照型での分岐、nullの明示的処理
Record Patterns Java 21 Recordの分解、ネストしたデータの抽出
ガード付きパターン Java 21 パターンへの追加条件付与
網羅性チェック Java 21 Sealed Classesとの連携による安全性保証

パターンマッチングを活用することで、以下のメリットが得られます。

  1. 簡潔なコード: 型チェック、キャスト、データ抽出を1つの式で表現
  2. 型安全性: コンパイラによる網羅性チェックで未処理のケースを防止
  3. 可読性の向上: 複雑な条件分岐を宣言的に記述
  4. リファクタリング安全性: 新しいサブタイプ追加時に影響箇所を自動検出

パターンマッチングは、RecordやSealed Classesと組み合わせることで真価を発揮します。これらの機能を活用して、より型安全で保守性の高いJavaコードを書いていきましょう。

参考リンク