Java21 で正式追加されたレコード・パターン(JEP 440: Record Patterns)

blog1.mammb.com


はじめに

Java16 で導入されたレコードクラス(JEP 395: Records)で、ようやくパターンマッチを使えるようになります。

JEP 440 Record Patterns は以下の経緯を経てリリースとなりました。

  • Java19 : JEP 405 としてファースト・プレビュー
  • Java20 : JEP 432 としてセカンド・プレビュー
    • ファースト・プレビューから、汎用レコードパターンの型引数の推論サポートを追加
    • ファースト・プレビューから、for文でのレコードパターンのサポートを追加
    • ファースト・プレビューから、名前付きレコードパターンのサポートを削除
  • Java21 : JEP 440 として正式リリース
    • セカンド・プレビューから、for文でのレコードパターンのサポート(for (Pair(var fst, var snd): arg) {})を削除

レコードパターンは、同じく Java21 で正式リリースとなる JEP 441 Pattern Matching for switch と合わせて使うことになりますが、ここでは JEP 440 Record Patterns の内容について見ていきます。

JEP 441 Pattern Matching for switch については以下を参照してください。

blog1.mammb.com


レコードパターン

以下のようなレコードクラスが有った場合、

record Point(int x, int y) {}

instanceof で以下のようにレコードのパターンマッチが利用できます。

static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x + y);
    }
}

Point(int x, int y) がレコードパターンです。

これは、対象コンポーネントのローカル変数宣言をパターン自体に持ち上げ、値がパターンにマッチしたときにアクセッサメソッドを呼び出して変数を初期化するものです。

実質的に、レコードパターンは、レコードのインスタンスを構成要素に分解する役割を持ちます。

レコードパターンを使わない場合は以下のように書く必要があります。

    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }


ネストされたレコードパターン

上記は単純な例でしたが、レコードパターンが真の威力を発揮するのは、複雑なオブジェクト・グラフをマッチングする場合です。

record Point(int x, int y) {}

enum Color { RED, GREEN, BLUE }

record ColoredPoint(Point p, Color c) {}

record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

Rectangle の持つ ColoredPoint は、先の例と同様に以下のように抽出できます。

if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
     System.out.println(ul.c());
}

ColoredPoint の持つ Color は、以下のように抽出できます。

if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
                           ColoredPoint lr)) {
    System.out.println(c);
}

このように、レコードパターンでは、ネストしたレコードコンポーネントをパターンと照合して、外側と内側のレコードを一度に分解することができます。


var による型推論でのマッチング

レコードパターンは var を使って、コンポーネントの型を指定せずにレコードコンポーネントとマッチさせることができます。

例えば、パターン Point(var a, var b) はパターン Point(int a, int b) の省略形となります。

if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c),
                           var lr)) {
    System.out.println("Upper-left corner: " + x);
}


generic レコードのマッチング

以下のジェネリックなレコードがあった場合、

record MyPair<S, T>(S fst, T snd) {};
static void recordInference(MyPair<String, Integer> pair){
    switch (pair) {
        case MyPair(var f, var s) -> 
        ...
    }
}

case MyPair(var f, var s) は、MyPair<String,Integer>(var f, var s) として推論されます。


以下のようなネストされたケースでも型推論は動作します。

record Box<T>(T t) {}

static void test1(Box<Box<String>> bbs) {
    if (bbs instanceof Box<Box<String>>(Box(var s))) {
        System.out.println("String " + s);
    }
}

ここでは、Box(var s) の型引数は String と推論されるので、パターン自体は Box<String>(var s) と推論されます。

型引数は省略できるので、以下のように簡素化できます。

static void test2(Box<Box<String>> bbs) {
    if (bbs instanceof Box(Box(var s))) {
        System.out.println("String " + s);
    }
}


マッチングが失敗するパターン

record Pair(Object x, Object y) {}

Pair p = new Pair(42, 42);

オブジェクト値を持つペアを、整数で初期化した p を考えます。

以下のパターンでは、文字列のパターンでマッチングしているため、マッチは失敗し、else 句の内容が実行されます。

if (p instanceof Pair(String s, String t)) {
    System.out.println(s + ", " + t);
} else {
    System.out.println("Not a pair of strings");
}

また、null 値はどのレコードパターンにもマッチしないため、pnull 値の場合も else 句の内容が実行されます。


exhaustive switch

JEP 441 Pattern Matching for switch により、 switch 式と switch ステートメントでレコードパターンを利用できます。

switch ブロックは、セレクタ式のすべての可能な値を扱う節を持つ必要があります(exhaustive)。

以下のような sealed クラスや通常のクラスが組み合わされたレコードが有った場合、

class A {}
class B extends A {}
sealed interface I permits C, D {}
final class C implements I {}
final class D implements I {}
record Pair<T>(T x, T y) {}

Pair<A> p1;
Pair<I> p2;

次の switch は網羅的ではないためエラーになります。

switch (p1) {  // Error!
    case Pair<A>(A a, B b) -> ...
    case Pair<A>(B b, A a) -> ...
}

両方とも A 型の 2 つの値を含むペアにはマッチしないためです。

インターフェース Isealed であるため、タイプ CD はすべての可能なインスタンスをカバーするので、以下は網羅的です。

switch (p2) {
    case Pair<I>(I i, C c) -> ...
    case Pair<I>(I i, D d) -> ...
}

switch (p2) {
    case Pair<I>(C c, I i) -> ...
    case Pair<I>(D d, C c) -> ...
    case Pair<I>(D d1, D d2) -> ...
}

一方、以下のケースは、両方とも D 型の2つの値を含むペアにはマッチしないので、網羅的ではありません。

switch (p2) {  // Error!
    case Pair<I>(C fst, D snd) -> ...
    case Pair<I>(D fst, C snd) -> ...
    case Pair<I>(I fst, C snd) -> ...
}