5 レコード・クラス

Java SE 14のプレビュー機能として導入されたレコード・クラスは、通常のクラスよりも厳格ではなくプレーン・データ集計をモデル化するのに役立ちます。Java SE 15では、ローカル・レコード・クラスなどの追加機能を使用してプレビュー機能が拡張されています。

ノート:

これは、設計、仕様および実装は完了しているが、永続的でないプレビュー機能であり、将来のJava SEリリースでは別の形式で存在するか、完全になくなる可能性があることを意味します。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。「プレビュー機能」を参照してください。

レコード・クラスに関する背景情報は、JEP 384を参照してください。

レコード・クラスは一連のフィールドを宣言し、適切なアクセッサ、コンストラクタ、equalshashCodeおよびtoStringメソッドが自動的に作成されます。このクラスは単純な「データキャリア」として機能することを意図しているため、フィールドはfinalです。

たとえば、次の2つのフィールドを持つレコード・クラスがあります:

record Rectangle(double length, double width) { }

矩形のこの簡潔な宣言は、次の標準クラスと同等です:

public final class Rectangle {
    private final double length;
    private final double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double length() { return this.length; }
    double width()  { return this.width; }

    // Implementation of equals() and hashCode(), which specify
    // that two record objects are equal if they
    // are of the same type and contain equal field values.
    public boolean equals...
    public int hashCode...

    // An implementation of toString() that returns a string
    // representation of all the record class's fields,
    // including their names.
    public String toString() {...}
}

レコード・クラス宣言は、名前、ヘッダー(コンポーネントと呼ばれる、クラスのフィールドをリストする)および本体で構成されます。

レコード・クラスは、次のメンバーを自動的に宣言します:

  • ヘッダーのコンポーネントごとに、次の2つのメンバーがあります:
    • private finalフィールド。
    • 名前とコンポーネントのタイプが同じpublicアクセッサ・メソッド。Rectangleレコード・クラスの例では、これらのメソッドはRectangle::length()およびRectangle::width()です。
  • 署名がヘッダーと同じである標準コンストラクタです。このコンストラクタでは、レコード・クラスをインスタンス化するnew式の各引数が、対応するprivateフィールドに割り当てられます。
  • equalsメソッドとhashCodeメソッドの実装。これらのメソッドは、2つのレコード・クラスのタイプが同じで、等しいコンポーネント値が含まれている場合、その2つが等しいことを指定します。
  • toString()メソッドの実装。すべてのレコード・クラスのコンポーネントとその名前の文字列表現が含まれます。

次の例のように、newキーワードを使用してレコード・オブジェクト(レコード・クラスのインスタンス)を作成します:

Rectangle r = new Rectangle(4,5);

レコード・クラスの標準コンストラクタ

次の例では、Rectangleレコード・クラスの標準コンストラクタを明示的に宣言します。lengthおよびwidthがゼロより大きいことを検証します。そうでない場合は、IllegalArgumentExceptionをスローします:
record Rectangle(double length, double width) {
    public Rectangle(double length, double width) {
        if (length <= 0 || width <= 0) {
            throw new java.lang.IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
        this.length = length;
        this.width = width;
    }
}

標準コンストラクタのシグネチャでレコード・クラスのコンポーネントを繰り返すと、退屈でエラーが発生しやすくなります。これを回避するには、シグネチャが暗黙的な(コンポーネントから自動的に導出される)コンパクト・コンストラクタを宣言します。

たとえば、次のコンパクト・コンストラクタ宣言では、前の例と同じ方法で、lengthおよびwidthを検証します:

record Rectangle(double length, double width) {
    public Rectangle {
        if (length <= 0 || width <= 0) {
            throw new java.lang.IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
    }
}

この簡潔な形式のコンストラクタ宣言は、レコード・クラスでのみ使用できます。標準コンストラクタに現れる文this.length = length;およびthis.width = width;は、コンパクト・コンストラクタには現れないことに注意してください。コンパクト・コンストラクタの最後に、その暗黙的仮パラメータが、そのコンポーネントに対応するレコード・クラスのprivateフィールドに割り当てられます。

レコード・クラス・メンバーの明示的な宣言

レコード・クラスのコンポーネントに対応するpublicアクセッサ・メソッドなど、ヘッダーから導出された任意のメンバーを明示的に宣言できます。次に例を示します:

record Rectangle(double length, double width) {
 
    // Public accessor method
    public double length() {
        System.out.println("Length is " + length);
        return length;
    }
}

独自のアクセッサ・メソッドを実装する場合は、暗黙的に導出されるアクセッサと同じ特性を持っていることを確認します(たとえば、publicと宣言され、対応するレコード・クラス・コンポーネントと同じ戻り型を持っていること)。同様に、独自のバージョンのequalshashCodeおよびtoStringメソッドを実装する場合は、すべてのレコード・クラスの共通スーパークラスであるjava.lang.Recordクラスの特性および動作と同じであることを確認します。

次の例のように、静的フィールド、静的イニシャライザおよび静的メソッドをレコード・クラスで宣言でき、これらは標準クラスの場合と同様に動作します。次に例を示します:

record Rectangle(double length, double width) {
    
    // Static field
    static double goldenRatio;

    // Static initializer
    static {
        goldenRatio = (1 + Math.sqrt(5)) / 2;
    }

    // Static method
    public static Rectangle createGoldenRectangle(double width) {
        return new Rectangle(width, width * goldenRatio);
    }
}

インスタンス変数(静的でないフィールド)またはインスタンス・イニシャライザは、レコード・クラスで宣言できません。

たとえば、次のレコード・クラス宣言はコンパイルされません:

record Rectangle(double length, double width) {

    // Field declarations must be static:
    BiFunction<Double, Double, Double> diagonal;

    // Instance initializers are not allowed in records:
    {
        diagonal = (x, y) -> Math.sqrt(x*x + y*y);
    }
}

独自のアクセッサ・メソッドを実装するかどうかに関係なく、レコード・クラスでインスタンス・メソッドを宣言できます。ネストされたクラスおよびインタフェースは、ネストされたレコード・クラス(暗黙的に静的)を含めて、レコード・クラスで宣言することもできます。次に例を示します:

record Rectangle(double length, double width) {

    // Nested record class
    record RotationAngle(double angle) {
        public RotationAngle {
            angle = Math.toRadians(angle);
        }
    }
    
    // Public instance method
    public Rectangle getRotatedRectangleBoundingBox(double angle) {
        RotationAngle ra = new RotationAngle(angle);
        double x = Math.abs(length * Math.cos(ra.angle())) +
                   Math.abs(width * Math.sin(ra.angle()));
        double y = Math.abs(length * Math.sin(ra.angle())) +
                   Math.abs(width * Math.cos(ra.angle()));
        return new Rectangle(x, y);
    }
}

レコード・クラスではnativeメソッドを宣言できません。

レコード・クラスの機能

レコード・クラスは暗黙的にfinalであるため、明示的に拡張できません。ただし、これらの制限事項を除けば、レコード・クラスは通常のクラスと同様に動作します:

  • 汎用レコード・クラスを作成できます。次に例を示します:

    record Triangle<C extends Coordinate> (C top, C left, C right) { }
  • 1つ以上のインタフェースを実装するレコード・クラスを宣言できます。次に例を示します:

    record Customer(...) implements Billable { }
  • レコード・クラスとその個々のコンポーネントに注釈を付けることができます。次に例を示します:

    record Rectangle(
        @GreaterThanZero double length,
        @GreaterThanZero double width) { }
    注釈がレコード・クラスのコンポーネントに適用される場合、自動的に宣言されるレコード・クラス内の多数のメンバーにも適用されます。この例では、@GreaterThanZero注釈はlengthおよびwidthコンポーネントだけでなく、次のコンポーネントにも適用されます:
    • 標準コンストラクタのパラメータ
    • レコード・クラスのコンポーネント
    • privateフィールドlengthおよびwidth
    • アクセッサ・メソッドlength()およびwidth()

    @Targetメタ注釈を使用して、注釈を適用するメンバーを指定できます。たとえば、@GreaterThanZeroがフィールド宣言にのみ適用される場合は、次のようになります:

    import java.lang.annotation.*;
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface GreaterThanZero { }

    前述のレコード・クラス宣言は、次の通常のクラス宣言と同等です:

    public final class Rectangle {
        private final @GreaterThanZero double length;
        private final @GreaterThanZero double width;
        
        public Rectangle(double length, double width) {
            this.length = length;
            this.width = width;
        }
        
        double length() { return this.length; }
        double width() { return this.width; }
    }

レコード・クラス、シール・クラスおよびインタフェース

レコード・クラスは、シール・クラスおよびインタフェースを適切に処理します。例については、許可されたサブクラスとしてのレコード・クラスを参照してください。

ローカル・レコード・クラス

ローカル・レコード・クラスはローカル・クラスに似ていますが、メソッド本体で定義できるレコード・クラスです。

次の例では、業者はレコード・クラスMerchantでモデル化されています。業者による販売も、レコード・クラスSaleを使用してモデル化されています。MerchantSaleはどちらも最上位のレコード・クラスです。業者とその月間売上合計の集計は、findTopMerchantsメソッド内で宣言されるローカル・レコード・クラスMonthlySalesを使用してモデル化されています。このローカル・レコード・クラスにより、後続のストリーム操作が読みやすくなります:

import java.time.*;
import java.util.*;
import java.util.stream.*;

record Merchant(String name) { }

record Sale(Merchant merchant, LocalDate date, double value) { }

public class MerchantExample {
    
    List<Merchant> findTopMerchants(
        List<Sale> sales, List<Merchant> merchants, int year, Month month) {
    
        // Local record class
        record MerchantSales(Merchant merchant, double sales) {}

        return merchants.stream()
            .map(merchant -> new MerchantSales(
                merchant, this.computeSales(sales, merchant, year, month)))
            .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
            .map(MerchantSales::merchant)
            .collect(Collectors.toList());
    }   
    
    double computeSales(List<Sale> sales, Merchant mt, int yr, Month mo) {
        return sales.stream()
            .filter(s -> s.merchant().name().equals(mt.name()) &&
                s.date().getYear() == yr &&
                s.date().getMonth() == mo)
            .mapToDouble(s -> s.value())
            .sum();
    }    

    public static void main(String[] args) {
        
        Merchant sneha = new Merchant("Sneha");
        Merchant raj = new Merchant("Raj");
        Merchant florence = new Merchant("Florence");
        Merchant leo = new Merchant("Leo");
        
        List<Merchant> merchantList = List.of(sneha, raj, florence, leo);
        
        List<Sale> salesList = List.of(
            new Sale(sneha,    LocalDate.of(2020, Month.NOVEMBER, 13), 11034.20),
            new Sale(raj,      LocalDate.of(2020, Month.NOVEMBER, 20),  8234.23),
            new Sale(florence, LocalDate.of(2020, Month.NOVEMBER, 19), 10003.67),
            // ...
            new Sale(leo,      LocalDate.of(2020, Month.NOVEMBER,  4),  9645.34));
        
        MerchantExample app = new MerchantExample();
        
        List<Merchant> topMerchants =
            app.findTopMerchants(salesList, merchantList, 2020, Month.NOVEMBER);
        System.out.println("Top merchants: ");
        topMerchants.stream().forEach(m -> System.out.println(m.name()));
    }
}

ネストされたレコード・クラスと同様に、ローカル・レコード・クラスは暗黙的に静的です。つまり、静的でないローカル・クラスとは異なり、独自のメソッドは包含するメソッドの変数にアクセスできません。

レコードのシリアライズ

レコード・クラスのインスタンスはシリアライズおよびデシリアライズできますが、writeObjectreadObjectreadObjectNoDatawriteExternalまたはreadExternalメソッドを提供してプロセスをカスタマイズすることはできません。レコード・クラスのコンポーネントはシリアライズを制御し、レコード・クラスの標準コンストラクタはデシリアライズを制御します。詳細および拡張例は、シリアライズ可能レコードを参照してください。『Javaオブジェクト直列化仕様』のセクションレコードの直列化も参照してください。

レコード・クラスに関連するAPI

abstractクラスjava.lang.Recordは、すべてのレコード・クラスの共通スーパークラスです。

ソース・ファイルがjava.lang以外のパッケージからRecordという名前のクラスをインポートすると、コンパイラ・エラーが発生する場合があります。Javaソース・ファイルは、暗黙的なimport java.lang.*;文を介して、java.langパッケージ内のすべての型を自動的にインポートします。これには、プレビュー機能が有効か無効かに関係なく、java.lang.Recordクラスが含まれます。

com.myapp.Recordの次のクラス宣言を考えてみます:
package com.myapp;

public class Record {
    public String greeting;
    public Record(String greeting) {
        this.greeting = greeting;
    }
}

次の例では、org.example.MyappPackageExampleはワイルドカードを使用してcom.myapp.Recordをインポートしますが、Java SE 15ではコンパイルしません:

package org.example;
import com.myapp.*;

public class MyappPackageExample {
    public static void main(String[] args) {
       Record r = new Record("Hello world!");
    }
}

コンパイラは、次のようなエラーメッセージを生成します:

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
       Record r = new Record("Hello world!");
       ^
  both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
       Record r = new Record("Hello world!");
                      ^
  both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

com.myappパッケージのRecordjava.langパッケージのRecordは、ワイルドカードを使用してインポートされます。したがって、どちらのクラスも優先されず、コンパイラは単純な名前Recordの使用を検出するとエラーを生成します。

この例をコンパイルできるようにするには、Recordの完全修飾名をインポートするようにimport文を変更します:

import com.myapp.Record;

ノート:

java.langパッケージでのクラスの導入はまれですが、Java SE 5ではEnum、Java SE 9ではModule、Java SE 14ではRecordなど、時々必要になります。

クラスjava.lang.Classには、レコード・クラスに関連する次の2つの新しいメソッドがあります:

  • RecordComponent[] getRecordComponents(): レコード・クラスのコンポーネントに対応するjava.lang.reflect.RecordComponentオブジェクトの配列を返します。
  • boolean isRecord(): クラスがレコード・クラスとして宣言された場合にtrueを返すことを除き、isEnum()に似ています。