はじめに#
従来の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網羅性チェック付き"]
endinstanceofの型パターン#
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のように、nullとdefaultを組み合わせることもできます。
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つの式で表現
- 型安全性: コンパイラによる網羅性チェックで未処理のケースを防止
- 可読性の向上: 複雑な条件分岐を宣言的に記述
- リファクタリング安全性: 新しいサブタイプ追加時に影響箇所を自動検出
パターンマッチングは、RecordやSealed Classesと組み合わせることで真価を発揮します。これらの機能を活用して、より型安全で保守性の高いJavaコードを書いていきましょう。
参考リンク#