※本記事は、“Bruce Eckel on Java records” の翻訳記事です。

2022年4月8日| 6分読む

Bruce Eckel


Javaにレコードが追加されたことによって除外されるボイラープレートとエラーは大量です。レコードを使用すれば、コードもはるかに読みやすくなります

 

[このシリーズでは、Java 8以降にJava言語に追加された新機能について説明します。2021年12月に出版された「On Java 8」の第2版に追加された資料から引用しています。これらの記事のソースコードおよびブック全体については、GitHubから、詳細なインストール手順とともに入手できます。 – 編集部]

JDK 16では、データ転送オブジェクト(データ・キャリアとも呼ばれる)になるように設計されたクラスを定義するrecordキーワードの追加が決定されました。レコードによって以下が自動生成されます。

  • 不変フィールド
  • 標準コンストラクタ
  • 各要素のアクセサ・メソッド
  •  equals() メソッド
  •  hashCode() メソッド
  •  toString() メソッド

次のコードは各機能を示しています。

// collections/BasicRecord.java
// {NewFeature} Since JDK 16
import java.util.*;

record Employee(String name, int id) {}

public class BasicRecord {
  public static void main(String[] args) {
    var bob = new Employee("Bob Dobbs", 11);
    var dot = new Employee("Dorothy Gale", 9);
    // bob.id = 12; // エラー:
    // id has private access in Employee
    System.out.println(bob.name()); // アクセサ
    System.out.println(bob.id()); // アクセサ
    System.out.println(bob); // toString()
    // Employee は Map のキーとして機能します:
    var map = Map.of(bob, "A", dot, "B");
    System.out.println(map);
  }
}

/* Output:
Bob Dobbs
11
Employee[name=Bob Dobbs, id=11]
{Employee[name=Dorothy Gale, id=9]=B, Employee[name=Bob Dobbs, id=11]=A}
*/

(注意:  {NewFeature} コメント・タグによって、JDK 8を使用するGradleビルドからこの例は除外されます。)

レコードを使う多くの場合、名前を付けてパラメータを指定するだけで、本文には何も必要ありません。これにより、 main()の最初の2行で呼び出される標準コンストラクタが自動的に作成されます。この使用方法では、内部の private final フィールドの名前とidも作成されます。コンストラクタは、引数リストからこれらのフィールドを初期化します。

フィールドをヘッダーで定義しない限り、レコードに追加することはできません。ただし、 static のメソッド、フィールドおよびイニシャライザは許可されています。

レコードの引数リストを介して定義された各プロパティは、 bob.name() および bob.id()の呼び出しで見られるように、独自のアクセサを自動的に取得します(設計者が getName()getId()といったアクセサの古いJavaBeanプラクティスを続けなかったことに感謝します)。

出力を見ると、レコードによってtoString() メソッドも作成されることがわかります。レコードは適切に定義されたhashCode() メソッドと equals() メソッドも作成するため、EmployeeMap. のキーとして使用できます。この Map が表示されると、 toString() メソッドによって読み取り可能な結果を生成します。

後でレコード内のフィールドの1つを加算、減算、または変更したとしても、Javaは結果が適切に機能することを保証します。この可変性は、レコードを非常に価値あるものにしている理由の1つです。

レコードではメソッドを定義できますが、そのメソッドでは、次のようにフィールドの読み取りしかできません。フィールドが自動でfinalになっているからです。

// collections/FinalFields.java
// {NewFeature} Since JDK 16
import java.util.*;

record FinalFields(int i) {
  int timesTen() { return i * 10; }
  // void tryToChange() { i++; } // エラー:
  // cannot assign a value to final variable i
}

レコードは、次のように他のレコードを含む他のオブジェクトで構成できます。

// collections/ComposedRecord.java
// {NewFeature} Since JDK 16

record Company(Employee[] e) {}

// class Conglomerate extends Company {}
// エラー: final のCompanyから継承することはできません

レコードは暗黙的に finalであるため(また abstractにはできないため)、レコードからは継承できません。また、レコードは別のクラスを継承できません。ただし、レコードでは次のようにインタフェースを実装できます。

// collections/ImplementingRecord.java
// {NewFeature} Since JDK 16

interface Star {
  double brightness();
  double density();
}

record RedDwarf(double brightness) implements Star {
  @Override public double density() { return 100.0; }
}

コンパイラによって density()の定義を強制されますが、 brightness()については強制されません。これは、レコードが自動的に引数 brightness のアクセサを生成し、そのアクセサが interface Starの brightness()の制約を満たすためです。

次の例に示すように、レコードはクラス内でネストすることも、メソッド内でローカルに定義することもできます。

// collections/NestedLocalRecords.java
// {NewFeature} Since JDK 16

public class NestedLocalRecords {
  record Nested(String s) {}
  void method() {
    record Local(String s) {}
  }
}

ネストされたレコードとローカルのレコードの使用はどちらも暗黙的にstaticです。

標準コンストラクタはレコードの引数に従って自動的に作成されますが、コンパクト・コンストラクタを使用してコンストラクタの動作を追加できます。コンパクト・コンストラクタは普通のコンストラクタのように見えますが、次のようにパラメータ・リストがありません。

// collections/CompactConstructor.java
// {NewFeature} Since JDK 16

record Point(int x, int y) {
  void assertPositive(int val) {
    if(val < 0)
      throw new IllegalArgumentException("negative");
  }
  Point { // コンストラクタ: パラメータ・リストがありません
    assertPositive(x);
    assertPositive(y);
  }
}

コンパクト・コンストラクタは通常、引数の検証に使用されます。コンパクト・コンストラクタを使用してフィールドの初期化値を変更することもできます。

// collections/PlusTen.java
// {NewFeature} Since JDK 16

record PlusTen(int x) {
  PlusTen {
    x += 10;
  }
  // フィールドの修正はコンストラクタでのみ発生します。
  // これは正しくありません:
  // void mutate() { x += 10; }
  public static void main(String[] args) {
    System.out.println(new PlusTen(10));
  }
}
/* Output:
PlusTen[x=20]
*/

これはfinal変数が変更されているかのように見えますが、実際には変更されていません。裏では、コンパイラがxの中間プレースホルダを作成し、コンストラクタの最後で this.x へ結果の割り当てを行っています。

必要に応じて、次のように通常のコンストラクタ構文を使用して標準コンストラクタを置き換えることができます。

// collections/NormalConstructor.java
// {NewFeature} Since JDK 16

record Value(int x) {
  Value(int x) { // パラメータ・リスト
    this.x = x; // 明示的に初期化する必要があります
  }
}

ご覧のとおり、こちらはやや不自然です。コンストラクタは、識別子名を含むレコードの署名を正確に複製する必要があります。 Value(int initValue)を使用して定義することはできません。さらに、record Value(int x) は、非コンパクト・コンストラクタの使用時に初期化されていないxという名前のfinal フィールドを生成するため、コンストラクタが this.xを初期化しない場合、コンパイル時にエラーが発生します。幸いなことに、通常のコンストラクタ・フォームをレコードとともに使用することはほとんどありません。コンストラクタを記述する場合はほぼ毎回コンパクト・フォームになり、このフォームがフィールドの初期化を行います。

レコードをコピーするには、すべてのフィールドをコンストラクタに明示的に渡す必要があります。

// collections/CopyRecord.java
// {NewFeature} Since JDK 16

record R(int a, double b, char c) {}

public class CopyRecord {
  public static void main(String[] args) {
    var r1 = new R(11, 2.2, 'z');
    var r2 = new R(r1.a(), r1.b(), r1.c());
    System.out.println(r1.equals(r2));
  }
}
/* Output:
true
*/

レコードを作成すると、コピーが元のレコードと同じであることを確認できる equals() メソッドが生成されます。

 

まとめ

レコードは、同様の構造が他の言語で有用であることが示された後に、Javaに追加されました。レコードの追加によって除外されるボイラープレートとエラーの量は膨大です。また、レコードによってコードがはるかに読みやすくなり、これまで同等のクラスの追加に抵抗していたであろう開発者が新しいレコード・タイプを追加することに躊躇しなくなります。

 

より詳しい情報