Java 16以降のRecord型:不変データクラスの宣言を一行で実現する手法

Javaでは、単純なデータ保持用クラス(例:座標、設定値、APIレスポンス)を作成する際、長年にわたり大量のボイラープレートコードが必要でした。フィールド宣言、コンストラクタ、アクセサ、equals/hashCode/toStringの実装——これらは機能的価値が低く、ミスの温床にもなり得ました。Java 14でプレビュー導入され、Java 16で正式採用されたrecord型は、この課題を根本から再設計し、意図を明示的に表現する新しい言語構文を提供します。

従来のアプローチ:冗長な手動実装

例えば、2次元平面上の位置を表すPositionクラスを従来方式で定義すると、以下のようなコードになります:

public final class Position {
    private final double latitude;
    private final double longitude;

    public Position(double latitude, double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public double getLatitude() { return latitude; }
    public double getLongitude() { return longitude; }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Position position = (Position) obj;
        return Double.compare(position.latitude, latitude) == 0 &&
               Double.compare(position.longitude, longitude) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(latitude, longitude);
    }

    @Override
    public String toString() {
        return "Position{latitude=" + latitude + ", longitude=" + longitude + "}";
    }
}

この実装は正しく動作しますが、本質的な情報(「緯度と経度を持つ位置」という意味)は、機械的なコードに埋もれてしまいます。

Recordによる宣言的記述

同じPositionrecordで記述すると、以下の通りです:

public record Position(double latitude, double longitude) {}

この1行だけで、コンパイラは次のすべてを自動生成します:

  • 全フィールドを引数に取るパブリックなコンストラクタ
  • 各コンポーネントに対応するpublicなアクセサメソッド(latitude(), longitude()
  • すべてのコンポーネント値に基づくequalsおよびhashCode
  • 名前付き値形式のtoString(例:Position[latitude=35.6895, longitude=139.6917]

制約と拡張性:設計上のバランス

recordは「透明なデータキャリア」であることを前提として設計されています。そのため、以下の特性を持ちます:

  • 暗黙の不変性:コンポーネントは自動的にfinalとなり、状態変更が不可能です。
  • 固定スーパークラス:すべてのrecordjava.lang.Recordを直接継承し、他のクラスの継承は許可されません。
  • インターフェース実装可能:ビジネス契約(例:Serializable, Comparable<Position>)を満たすために、任意のインターフェースを実装できます。
  • カスタムロジックの追加可能:静的メソッド、インスタンスメソッド、バリデーション付きのコンパクトコンストラクタを定義できます。

以下は、地理座標の有効性チェックを含む拡張例です:

public record Position(double latitude, double longitude) 
    implements Comparable<Position> {

    // コンパクトコンストラクタ:インスタンス生成時の検証
    public Position {
        if (latitude < -90.0 || latitude > 90.0) {
            throw new IllegalArgumentException("Latitude must be in [-90, 90]");
        }
        if (longitude < -180.0 || longitude > 180.0) {
            throw new IllegalArgumentException("Longitude must be in [-180, 180]");
        }
    }

    // カスタム比較:緯度優先ソート
    @Override
    public int compareTo(Position other) {
        int latCmp = Double.compare(this.latitude, other.latitude);
        return latCmp != 0 ? latCmp : Double.compare(this.longitude, other.longitude);
    }

    // ユーティリティメソッド
    public boolean isNearEquator() {
        return Math.abs(latitude) < 5.0;
    }
}

適切な適用範囲

recordは万能ではありません。以下の用途に特に適しています:

  • レイヤー間通信のためのDTO(例:REST APIのリクエスト/レスポンス)
  • 関数型スタイルの処理における中間データ構造(例:Stream<Position>.map(...)
  • 複数値の返却(例:record Result<T>(boolean success, T data, String error)
  • 構造化ログや監査用のイベントペイロード

一方、状態遷移や振る舞いのカプセル化が必要なエンティティクラス(例:BankAccount)には不向きです。そのようなケースでは、依然として通常のクラス設計が推奨されます。

タグ: java-record java-16 immutable-data java-dto java-se

6月12日 16:51 投稿