Javaリハビリ(レコード編)

2023/01/13に公開

はじめに

数年ぶりにJavaに触れることになりましたが、その年月の間にバージョンアップを重ねて多くの新機能が追加されているようです。私自身のリハビリメモとしてこれらの機能を紹介勉強していきたいと思います。今回はレコードです。

レコードとは

こんなやつです。

Point.java
package cafebabe.zenn;

/**
 * Point
 */
public record Point(int x, int y) {}

...なんて素敵なんだ!!
シンプルすぎます。レコードが導入される以前は、上記と同じコードを以下のように記述する必要がありました。

Point.java
package cafebabe.zenn;

/**
 * Point
 */
class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int x() { return x; }
    int y() { return y; }

    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return other.x == x && other.y == y;
    }

    public int hashCode() {
        return Objects.hash(x, y);
    }

    public String toString() {
        return String.format("Point[x=%d, y=%d]", x, y);
    }
}

...なんて面倒なんだ!!
xyの座標(Pointクラス)を定義するだけで、上記のコード量です。この定型コード(ボイラープレート)の多さがJavaに対する1つの不満でした。それゆえ、Lombokのようなライブラリが利用されるケースもありました。

上記のコードは以下から引用(一部改変)させて頂いています。
https://openjdk.org/jeps/384

レコード=タプル

Javaのレコードは他の言語でお馴染みのタプルとして捉えることができます。タプルとは複数の要素からなる順序付けられた組のことで、基本的に変更不可なデータ型です。例えば、Pythonでは、上記のPointレコードは以下のタプルとして表現できます。

Point = Tuple[int, int]
p1: Point = (1,2)

Javaのレコード=名前付きタプル

JEP 384にもあるように、名前付きタプル(Nominal Tuple)と呼ばれます。「名前付き」とは何を意味するのでしょうか?

Pythonのタプルは名前付きではありません。従って、以下のような異なる型でもその構造が同じであれば置換可能です。(Pythonの型は実行環境では意味を持たないので、mypyなどで型チェックしたケースです)

AnotherPoint = Tuple[int, int]
p2: AnotherPoint = (2,3)
p2 = p1

一方、Javaのレコードは以下のようなコードはエラーとなります。

Point.java
package cafebabe.zenn;

/**
 * Point
 */
public record Point(int x, int y) {}
AnotherPoint.java
package cafebabe.zenn;

/**
 * AnotherPoint
 */
public record AnotherPoint(int x, int y) {}
void これはエラー() {
    var p1 = new Point(1,2);
    var p2 = new AnotherPoint(2, 3);
    p1 = p2;
}

PointレコードとAnotherPointレコードはその構造は全く同じですが、レコード名が違うために置換できません。これが「名前付き」の意味するところです。

その他の特徴

  • コンストラクタやアクセサが暗黙的に生成されます。
  • equalsやhashCodeメソッドも暗黙的に生成されます。
  • 値を変更することはできません。レコードは不変オブジェクトです。
    var p1 = new Point(1,2);
    var p2 = new Point(1,2);
        
    int x = p1.x(); //accessorは自動生成
    int y = p1.y(); 

    assert p1.equals(p2); //equalsメソッドも自動生成      

ローカルレコード

以下のように、レコードはメソッド内でも定義できるようです。
(またまた、こちらからの引用です)

List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
    // Local record
    record MerchantSales(Merchant merchant, double sales) {}

    return merchants.stream()
        .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
        .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
        .map(MerchantSales::merchant)
        .collect(toList());
}

上記のようにリストやマップの変換(処理)過程では、中間的な値が発生するケースがよくあります。このようなちょっとした中間的な値を表現する(上記の例ではMerchantSales)ために、ローカルレコードは最適だと思います。(Pythonなど他の言語ではもっと簡単にタプルで表現できますが...)

ユースケース

私が現時点で思う使い所です。

  • レコードは不変(Immutable)、つまり変更できません。従って、不変な値オブジェクトを表現することができます。
  • 複数の値を戻り値とするメソッドの戻り値の軽量な型をさくっと定義するのに便利です。レコード以前は、配列やリストなどを使っていたかと思います。
  • 前節で見たローカルレコードもちょっとした中間データの表現に適しています。

まとめ

今回は、Javaのレコードについて紹介勉強しました。その特徴をもう一度振り返りましょう。

  • Javaのレコードは名前付きタプルです。
  • Javaのレコードは不変です。
  • コンストラクタ、アクセッサ、equalsメソッドなどが自動生成されます。
  • メソッド内部でレコードを定義できます。(ローカルレコード)

また、考えられるユースケースについても触れました。

  • 値オブジェクトの表現
  • 複数の値を返すメソッドの戻り型の表現
  • データ加工中の中間データの表現

Java19ではレコードのパターンマッチング なども取り入れられているようです。Javaも生まれて30年ですが、複雑さよりも扱いやすさに今後も重点を入れていくのでしょうか・・・?

Discussion