2020/04/17

Java 14 の新機能 (7) - JEP 359: Records (Preview)

JEP 359: Records (Preview)

record とは

Java 14 から、データを保持するための immutable なクラスを定義するための新しい構文が追加されました。それが record です。
データを保持するために、以下のような性質を持つクラスを定義することは、Java では頻繁にあります。

  • immutable であること
    • コンストラクタでフィールドを全て設定し、フィールドは final
  • 値を取り出すメソッドを持っていること
  • final クラスであること
  • hashCode, equals, toString が適切に定義されていること
とても頻繁に定義するので、IDE で自動生成したり、Lombok のような特殊な仕組みを使ったりしていました。
これからは、record がサポートしてくれます。

record を使わないと……

例えば、Point というデータクラスを考えます。x と y の2つのフィールドを持っていて、immutable です。instanceof のパターンマッチングまで使ってがんばって小さく定義してみましたが、だいたい以下のような形になるかと思います。

public final class Point {
  private final int x, y;
  public Point(final int x, final int y) {
    this.x = x;
    this.y = y;
  }
  public int x() {
    return x;
  }
  public int y() {
    return y;
  }
  @Override
  public int hashCode() {
    return java.util.Arrays.hashCode(new Object[] { x, y });
  }
  @Override
  public boolean equals(Object obj) {
    return this == obj || obj != null && obj instanceof Point other && x == other.x && y == other.y;
  }
  @Override
  public String toString() {
    return "Point[x=" + x + ", y=" + y + "]";
  }
}

Lombok だと、こうでしょうか。
@lombok.Value
public final class Point {
  private int x, y;
}

record ではこうなる!

record Point(int x, int y) {
}

ここまでコンパクトに書けます。これは楽ですねー。
メンバが多いと、フィールドの書式で書きたい気もしますが、まぁいいでしょう。

record コンストラクタ

入力値のチェックなどを行うため、コンストラクタを定義することができます。

record Point(int x, int y) {
  public Point {
    if (x < 0 || y < 0) {
      throw new IllegalArgumentException("x and y cannot be negative.");
    }
  }
}

record のコンストラクタの引数は省略されます。この例だと x, y を引数のようにそのまま扱います。record は追加のフィールドを定義できないので、導出フィールドをコンストラクタで計算して保持するようなことはできません (private final int total を定義して、コンストラクタで total = x + y のようなことはできません)。

メソッドは定義できる

導出フィールドは定義できませんが、メソッドは定義できるので、public int total() { return x + y; } とすることは出来ます。
ちなみに、static フィールドは定義できます。

アクセサが getXxx() ではなく、xxx()

ちょっとした注意点ですが、x, y に対するアクセサは getX(), getY() ではなく、x(), y() です。もう JavaBean Specification なんて知らんということでしょうか。それならメソッドである必要すらなく、final field を直接アクセスさせてもいいんじゃないかという気もしますが、どうなんでしょう。

積極的に使っていきたい

record が導入されたからといって、Lombok が要らなくなるわけではなく、アクセサが getXxx() 形式ではないことも何か問題が起きそうなので、全体としてちょっと微妙な気はします。
ですが、record として定義されていれば、一定の制約の下で定義されているので安心して immutable だと思って扱うことができるというのは、大きな価値だと思います。

record の真価は sealed 型の導入後

なお、record が真価を発揮するのは、JEP 360: Sealed Types (Preview) が導入されたときだと思います。Kotlin 風の sealed 型が定義できるようになるのですが、定義された sealed 型を持たせる末端のクラスは、record の条件を満たすことが多く、このコンパクトな記法で定義できることは、とても有益です。
sealed 型がリリースされたら、また詳しく取り上げたいと思います。