S-JIS[2023-09-23] 変更履歴

レコードパターン

Javaレコードのパターンマッチングについて。


概要

Java21(プレビュー版ではJava19)で、instanceofswitch文/式でパターンマッチングによるレコードの分解(フィールドの抽出)が出来るようになった。

record Point(int x, int y) {}
    static void printPoint(Object o) {
        if (o instanceof Point(var x, var y)) {
            System.out.printf("(%d, %d)%n", x, y);
        }

        switch(o) {
        case Point(var x, var y):
            System.out.printf("(%d, %d)%n", x, y);
            break;
        default:
            break;
        }
    }

instanceofやcaseの後にレコード名を書き、その直後に丸括弧で囲んでフィールドから受け取る変数を書く。
この変数(の個数とデータ型)はレコードのフィールドと一致している必要がある。(変数名が一致する必要は無い)


instanceofの例

instanceofの例。

record Point(int x, int y) {}
同等の例 備考
if (o instanceof Point(int x, int y)) {
  System.out.printf("(%d, %d)%n", x, y);
}
 
if (o instanceof Point p) {
  System.out.printf("(%d, %d)%n", p.x(), p.y());
}
 
 
double len = (o instanceof Point(int x, int y)) ?
  Math.sqrt(x*x + y*y) : 0;
 
double len = (o instanceof Point p) ?
  Math.sqrt(p.x()*p.x() + p.y()*p.y()) : 0;
 
条件演算子(三項演算子)でも使用可能。

switchの例

switchの例。

record Record1(int n) {}
record Record2(int n) {}
    static void test(Object o) {
        switch (o) {
        case Record1(int i):
            System.out.println("rec1=" + i);
            break;
        case Record2(int i):
            System.out.println("rec2=" + i);
            break;
        default:
            System.out.println("default");
            break;
        }
    }

フィールドの抽出に使用する変数名は、異なるcaseであれば同じ変数名を使用可能。
(通常、switchブロックの中では、(異なるcaseの下であっても)同じ名前のローカル変数を複数定義できない(コンパイルエラーになる))


ただし、フィールドの抽出に使う変数が同じ変数名で同じデータ型であっても、fall throughさせることは出来ない。

        switch (o) {
        case Record1(int i): // コンパイルエラー
        case Record2(int i): // コンパイルエラー
            System.out.println("rec=" + i);
            break;
        default:
            System.out.println("default");
            break;
        }

RecordPatternExample.java:22: エラー: パターンからの不正なfall-through
        case Record1(int i):
             ^
RecordPatternExample.java:23: エラー: パターンに対して不正なfall-through
        case Record2(int i):
             ^

case Record1(int i), Record2(int i):」も駄目。

RecordPatternExample.java:22: エラー: 既存の一致バインディングを再定義することはできません
        case Record1(int i), Record2(int i):
                             ^

レコードのパターンマッチングの例。
(instanceof・switch等で共通)


データ型

レコードのフィールドを受け取る変数のデータ型は、フィールドの型と一致している必要がある。
(変数名が一致している必要は無い)

record RecordInt1(int n) {}
    static void test(Object o) {
        switch (o) {
        case RecordInt1(int i):
            System.out.println("rec1=" + i);
            break;
        default:
            break;
        }
    }

レコードのフィールドをvarで受け取ることが出来る。

        switch (o) {
        case RecordInt1(var i):
            System.out.println("rec1=" + i);
            break;
        default:
            break;
        }

varと直接のデータ型指定は混在させることが出来る。
ラムダ式では混在できないが)

record RecordInt2(int n, int m) {}
        switch (o) {
        case RecordInt2(var i, int j):
            System.out.println(i + j);
            break;
        default:
            break;
        }

varを使わない場合、フィールドがプリミティブ型なら、全く同じデータ型を指定しなければならない。
例えばレコードのフィールドがintなのにlongで受けることは出来ない。(ラッパークラスも駄目)(コンパイルエラーになる)

        switch (o) {
        case RecordInt1(long i): // コンパイルエラー
            System.out.println("rec1=" + i);
            break;
        }

Example.java:36: エラー: 不適合な型: 型longのパターンはintでは適用できません
                case RecordInt1(long i):
                                ^

継承関係にあるクラス(やインターフェース)は指定可能で、期待通りマッチする。
(明らかに代入できない(継承関係にない)クラスを指定するとコンパイルエラーになる)

record RecordS(CharSequence s) {}
        switch (o) {
//×    case RecordS(Integer s): // コンパイルエラー
        case RecordS(String s):  // OK
            System.out.println("string=" + s);
            break;
        default:
            System.out.println("default");
            break;
        }

レコードのネスト

レコードの中にレコードがある場合は、内側のレコードを指定してフィールドを抽出できる。

record Point(int x, int y) {}
record Rectangle(Point leftTop, Point bottomRight) {}
    if (o instanceof Rectangle(Point(var x1, var y1), Point(var x2, var y2))) {
        System.out.printf("(%d, %d)-(%d, %d)%n", x1, y1, x2, y2);
    }
// 同等の書き方の例
    if (o instanceof Rectangle r) {
        System.out.printf("(%d, %d)-(%d, %d)%n", r.leftTop().x(), r.leftTop().y(), r.bottomRight().x(), r.bottomRight().y());
    }
    if (o instanceof Rectangle(var p1, var p2)) {
        System.out.printf("(%d, %d)-(%d, %d)%n", p1.x(), p1.y(), p2.x(), p2.y());
    }

    // 一部をレコードのフィールドとして抽出し、一部をレコード自体として受け取ることも可能
    if (o instanceof Rectangle(Point(var x1, var y1), var p2)) {
        System.out.printf("(%d, %d)-(%d, %d)%n", x1, y1, p2.x(), p2.y());
    }

固定値

一部のフィールドに定数を指定して、その定数のときだけマッチするという書き方は出来ない。

record RecordInt2(int n, int m) {}
        switch (o) {
//×    case RecordInt2(123, int j): // 定数を書くとコンパイルエラー
        case RecordInt2(int i, int j) when i == 123: // ガードパターンならOK
            System.out.println("rec.m=" + j);
            break;
        default:
            System.out.println("default");
            break;
        }

ジェネリクス

ジェネリクスが使われているレコードをパターンマッチングする場合、基本的に型引数を除去してcaseに記述する。

record Box<T>(T t) {}
        test(new Box<>("abc"));
        test(new Box<>(123));
    static void test(Box<Object> box) {
        // switch
        switch (box) {
        case Box(String s):
            System.out.println("Box(String s)=" + s);
            break;
        case Box(Integer n):
            System.out.println("Box(Integer n)=" + n);
            break;
        default:
            break;
        }

        // instanceof
        if (box instanceof Box(String s)) {
            System.out.println("Box(String s)=" + s);
        }
    }
  Box<Object> box Box<String> box Box<?> box
case Box(String s):
case Box<>(String s): × × ×
case Box<Object>(String s): × ×
case Box<String>(String s): × ×
case Box<String>(var s): × ×
case Box(Integer n): ×

レコードへ戻る / Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま