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による宣言的記述
同じPositionをrecordで記述すると、以下の通りです:
public record Position(double latitude, double longitude) {}
この1行だけで、コンパイラは次のすべてを自動生成します:
- 全フィールドを引数に取るパブリックなコンストラクタ
- 各コンポーネントに対応する
publicなアクセサメソッド(latitude(),longitude()) - すべてのコンポーネント値に基づく
equalsおよびhashCode - 名前付き値形式の
toString(例:Position[latitude=35.6895, longitude=139.6917])
制約と拡張性:設計上のバランス
recordは「透明なデータキャリア」であることを前提として設計されています。そのため、以下の特性を持ちます:
- 暗黙の不変性:コンポーネントは自動的に
finalとなり、状態変更が不可能です。 - 固定スーパークラス:すべての
recordはjava.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)には不向きです。そのようなケースでは、依然として通常のクラス設計が推奨されます。