はじめに
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 については以下を参照してください。
レコードパターン
以下のようなレコードクラスが有った場合、
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
値はどのレコードパターンにもマッチしないため、p
が null
値の場合も 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 つの値を含むペアにはマッチしないためです。
インターフェース I
は sealed
であるため、タイプ C
と D
はすべての可能なインスタンスをカバーするので、以下は網羅的です。
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) -> ... }