1. record クラスを掘り下げる
record patterns に入る前に、record クラスとは何かをおさらいしましょう。
Record はコンパクトで不変なクラスで、equals、hashCode、toString が自動生成され、すべてのコンポーネントに対するゲッターも自動で用意されます。Record クラスは Java 16 で登場し、手書きの DTO にうんざりしていた開発者にとっての本当の贈り物となりました。
// 定番: 2 つのフィールドを持つ Point
record Point(int x, int y) {}
オブジェクトの生成とフィールドへのアクセス:
Point p = new Point(10, 20);
System.out.println(p.x()); // 10
System.out.println(p.y()); // 20
System.out.println(p); // Point[x=10, y=20]
Record Patterns が必要な理由
Java 21 以前は、オブジェクトが特定の record かどうかを確認してそのフィールドを得るには、instanceof による型チェック、キャスト、ゲッター呼び出しと、手作業のコードを多く書く必要がありました。
Object obj = new Point(5, 7);
if (obj instanceof Point) {
Point p = (Point) obj;
int x = p.x();
int y = p.y();
System.out.println("x=" + x + ", y=" + y);
}
record が複雑または入れ子の場合、コードはすぐに「アンパックの森」になってしまいます。さらに、こうした判別を switch で使うのは以前は不便でした。
Record patterns を使うと、record クラスの値をパターンマッチングの中でそのままアンパックできます。これは instanceof と switch の両方で使え、コードはより短く、安全で、読みやすくなります。
2. record patterns の構文: 最も簡単な例
record Point(int x, int y) {}
Object obj = new Point(10, 20);
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y);
}
ここで何が起きているか
- obj instanceof Point(int x, int y) — 型を確認しつつ、コンポーネントを変数 x と y に即座にアンパックします。
- 変数 x と y は if ブロックの内部でのみ有効です。
結果:
x=10, y=20
注意: obj が Point でない、または null の場合、if ブロックは実行されず、変数は宣言されません。
3. switch での Record patterns: 簡潔さと安全性
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
Object shape = new Point(3, 4);
switch (shape) {
case Point(int x, int y) -> System.out.println("Point: x=" + x + ", y=" + y);
case Circle(Point center, int r) -> System.out.println("Circle with radius " + r);
default -> System.out.println("Unknown shape!");
}
- case の Point(int x, int y) では、x と y にすぐアクセスできます。
- case の Circle(Point center, int r) では、center と r を参照できます。
結果:
Point: x=3, y=4
sealed 階層と組み合わせると、switch は網羅的になり、ケースの扱い漏れがあるとコンパイラが警告してくれます。
4. 入れ子の record patterns: アンパックの中でアンパック
record Point(int x, int y) {}
record Line(Point start, Point end) {}
Object obj = new Line(new Point(1, 2), new Point(3, 4));
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
System.out.println("Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
}
結果:
Line from (1,2) to (3,4)
木構造、グラフ、複雑なデータ構造を扱っていますか?入れ子のパターンは強力な味方です。
5. ガード条件 (when) を伴う record patterns
Object obj = new Point(0, 100);
if (obj instanceof Point(int x, int y) && x == 0) {
System.out.println("Y 軸上の点: y=" + y);
}
switch では when を使います:
switch (obj) {
case Point(int x, int y) when x == 0 -> System.out.println("Y 軸上: y=" + y);
case Point(int x, int y) when y == 0 -> System.out.println("X 軸上: x=" + x);
case Point(int x, int y) -> System.out.println("通常の点");
default -> System.out.println("点ではない");
}
6. 実アプリケーションでの record patterns の活用
実用的な例として、幾何学図形を扱ってみます。
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
record Rectangle(Point topLeft, int width, int height) {}
public static void printShapeInfo(Object shape) {
switch (shape) {
case Point(int x, int y) -> System.out.println("Point: (" + x + ", " + y + ")");
case Circle(Point(int x, int y), int radius) ->
System.out.println("Circle: center=(" + x + ", " + y + "), radius=" + radius);
case Rectangle(Point(int x, int y), int width, int height) ->
System.out.println("Rectangle: topLeft=(" + x + ", " + y + "), size=" + width + "x" + height);
default -> System.out.println("Unknown shape");
}
}
呼び出し例:
printShapeInfo(new Rectangle(new Point(5, 10), 20, 30));
結果:
Rectangle: topLeft=(5, 10), size=20x30
7. record patterns の制約と注意点
record クラス専用。 Record patterns は、本当に record クラスであるオブジェクトに対してのみ機能します。
class NotARecord {
int a, b;
NotARecord(int a, int b) { this.a = a; this.b = b; }
}
// エラー! record ではない
// if (obj instanceof NotARecord(int a, int b)) { ... }
コンポーネントの数と型が一致していること。 パターンは record クラスのコンポーネントに一致していなければなりません。
record Pair(int a, String b) {}
// エラー: 型が一致しない
// if (obj instanceof Pair(String a, int b)) { ... }
変数はブロック内でのみ有効。 パターンで宣言された名前は、該当の if や case の本体でマッチが成功した場合にのみ参照できます。
8. 入れ子パターン: 木の例
sealed interface Expr permits NumberExpr, PlusExpr, MinusExpr {}
record NumberExpr(int value) implements Expr {}
record PlusExpr(Expr left, Expr right) implements Expr {}
record MinusExpr(Expr left, Expr right) implements Expr {}
int eval(Expr expr) {
return switch (expr) {
case NumberExpr(int value) -> value;
case PlusExpr(Expr left, Expr right) -> eval(left) + eval(right);
case MinusExpr(Expr left, Expr right) -> eval(left) - eval(right);
};
}
使用例:
Expr e = new PlusExpr(new NumberExpr(7), new MinusExpr(new NumberExpr(10), new NumberExpr(3)));
System.out.println(eval(e)); // 7 + (10 - 3) = 14
ここでは NumberExpr(int value)、PlusExpr(Expr left, Expr right)、MinusExpr(Expr left, Expr right) をその場でアンパックしており、コードは簡潔で表現力豊かになります。
9. 表: record patterns あり/なしの pattern matching の比較
| 方法 | 手動の型キャスト | record patterns を用いた pattern matching |
|---|---|---|
| 型チェック | |
|
| キャスト | |
不要 |
| フィールドへのアクセス | |
|
| 入れ子のアンパック | 手作業のコードが多い | 入れ子のパターン |
| switch での利用 | 不便/不可能 | 容易で簡潔 |
10. record patterns のよくある誤り
誤り 1: record でないクラスに record pattern を適用する。 通常のクラスを書いてからそれを record のように「アンパック」しようとすると、コンパイラは許可しません。Record patterns は本物の record クラスに対してのみ動作します。
誤り 2: コンポーネントの数または型が一致しない。 パターンは record のコンポーネント宣言に完全一致していなければなりません。例えば、record Pair(int a, String b) に対して Pair(int x, int y) と書くことはできません。
誤り 3: パターンの変数をブロック外で使おうとする。 パターンで宣言された名前は、対応する if/case の内部でのみ有効です。ブロック外ではそのような変数は存在しません。
誤り 4: 古い JDK で record patterns を使う。 サポートは Java 21 で導入されました。Java 17 以下では構文エラーになります。JDK のバージョンと IDE のサポートを確認してください。
誤り 5: 入れ子のパターンが深すぎる。 入れ子は強力ですが、不要に複雑化しないでください。構造が深すぎると可読性が低下し、ミスのリスクが高まります。
GO TO FULL VERSION