Java | Java 詳細・モダン文法:言語仕様詳細 – レコード(record)

Java Java
スポンサーリンク

record を一言でいうと

record
「フィールドを持つだけの“データ用クラス”を、最小限のコードで定義するための仕組み」
です。

equals / hashCode / toString / コンストラクタ / getter を、
「全部コンパイラが自動生成してくれるクラス」と思ってください。

不変(immutable)な値オブジェクトを作るときに、圧倒的にコード量が減ります。


まずは普通のクラスとの違いを体感する

従来の「データクラス」がどれだけ冗長だったか

例えば、「ユーザー」を表すクラスを素直に書くと、こうなります。

public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() {
        return name;
    }

    public int age() {
        return age;
    }

    @Override
    public boolean equals(Object o) { ... }

    @Override
    public int hashCode() { ... }

    @Override
    public String toString() { ... }
}
Java

「ただ name と age を持ちたいだけ」なのに、
ボイラープレート(定型文)がやたら多いですよね。

record で書くと 1 行になる

同じものを record で書くと、こうなります。

public record User(String name, int age) {}
Java

たったこれだけ。

これだけで、コンパイラが自動的に

  • private final なフィールド
  • 全フィールドを引数に取るコンストラクタ
  • 各フィールド名と同じ名前のアクセサ(name() / age()
  • equals / hashCode / toString

を生成してくれます。


record の本質:「データの形」を宣言するクラス

「状態」ではなく「値」を表す

record は、「状態が変わるオブジェクト」ではなく
「ある時点の“値”を表すオブジェクト」を作るための仕組みです。

例えば、Point という 2 次元座標を表す record を考えます。

public record Point(int x, int y) {}
Java

これは「x と y の組」を表す“値”です。
一度作ったら、あとから x や y を変えることはできません(フィールドは暗黙に final)。

Point p = new Point(10, 20);
// p.x = 30; // そもそもフィールドに直接アクセスできないし、変更もできない
int x = p.x(); // アクセサで読むだけ
Java

「不変な値オブジェクト」として扱えるので、
コレクションのキーにしたり、並列処理で共有したりしても安全です。

equals / hashCode / toString が「中身ベース」で自動生成される

User u1 = new User("Alice", 20);
User u2 = new User("Alice", 20);

System.out.println(u1.equals(u2)); // true
System.out.println(u1);            // User[name=Alice, age=20]
Java

record の equals は「全フィールドが等しいか」で判定し、
toString は「クラス名と全フィールドの値」をいい感じに出してくれます。

「値オブジェクト」としての振る舞いが、何も書かなくても手に入るのが record の強みです。


record の構文と自動生成されるもの

基本形

public record User(String name, int age) {}
Java

この 1 行から、コンパイラは次のようなものを生成します(イメージ)。

public final class User implements java.io.Serializable {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() { return name; }
    public int age() { return age; }

    @Override
    public boolean equals(Object o) { ... }

    @Override
    public int hashCode() { ... }

    @Override
    public String toString() { ... }
}
Java

重要なのは、

  • クラスは final(継承できない)
  • フィールドは private final
  • アクセサは getName() ではなく name() という名前

という点です。

コンパクトコンストラクタで「入力チェック」を足す

「コンストラクタでバリデーションしたい」こともありますよね。
record では「コンパクトコンストラクタ」という形で書けます。

public record User(String name, int age) {
    public User {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name must not be blank");
        }
        if (age < 0) {
            throw new IllegalArgumentException("age must be >= 0");
        }
    }
}
Java

ここでの public User { ... } は、
「全フィールドを引数に取るコンストラクタ」の中身だけを定義しているイメージです。

コンパイラが

public User(String name, int age) {
    // フィールド代入前に、ここで引数を受け取る
    if (...) ...
    this.name = name;
    this.age = age;
}
Java

という形に展開してくれます。


record を使うと設計がどう変わるか

「DTO」「レスポンス」「設定値」などに相性がいい

例えば、API のレスポンスを表すクラス。

public record UserResponse(long id, String name, int age) {}
Java

設定値をまとめたクラス。

public record AppConfig(String baseUrl, int timeoutSeconds) {}
Java

こういった「ただのデータの入れ物」は、
record にすると意図がとてもクリアになります。

「このクラスはロジックを持つ“オブジェクト”ではなく、
ただの“データの形”を表しているだけだよ」
というメッセージを、コードで表現できるわけです。

「不変であること」が前提になる

record はフィールドが final なので、
「あとから setter で書き換える」という設計はできません。

これは制約であると同時に、大きなメリットでもあります。

  • どこかで勝手に書き換えられる心配がない
  • equals / hashCode の結果が途中で変わらない
  • 並列処理で共有しても安全

「このデータは作ったら変えない」という前提があるなら、
class より record を選ぶ方が、設計として筋が良いことが多いです。


record で「やってはいけないこと」「できないこと」

継承はできない(final)

record は暗黙に final なので、継承できません。

public record User(String name, int age) {}

// class PremiumUser extends User {} // コンパイルエラー
Java

「共通のフィールドを持つサブクラスをたくさん作りたい」といった用途には向きません。
そういうときは、普通のクラス+継承/コンポジションを使うべきです。

フィールドを増やしたり減らしたりできない(ヘッダが“型”)

record の「ヘッダ」に書いたフィールドが、その record の“型”そのものです。

public record User(String name, int age) {}
Java

あとから「やっぱり email も欲しい」と思ったら、
ヘッダを変える必要があります。

public record User(String name, int age, String email) {}
Java

これは「型が変わる」ので、
その record を使っている全てのコードに影響します。

「フィールドを増やしたり減らしたりしながら進化させたい」
というよりは、
「ある時点での“データの形”をガチッと決める」
という用途に向いています。


record を使うときの判断基準

自分にこう問いかけてみる

そのクラスを作るときに、こう自問してみてください。

「このクラスは、“振る舞いを持つオブジェクト”か?
それとも、“値を運ぶだけの入れ物”か?」

後者なら、record を強く検討していいです。

  • フィールドはコンストラクタで全部埋まる
  • 作ったあとに状態を変えない
  • equals / hashCode / toString はフィールドベースでよい

この 3 つに「はい」と言えるなら、record 向きです。

逆に、

  • 状態を変えながら使いたい(setter が欲しい)
  • 継承してポリモーフィズムを使いたい
  • equals / hashCode を独自ルールにしたい

といった場合は、普通のクラスを選ぶ方がいいです。


まとめ:record を自分の言葉で説明するなら

あなたの言葉で record を説明すると、こうなります。

record は、“データを運ぶだけの不変クラス”を、
record User(String name, int age) のように 1 行で定義できる仕組み。
コンパイラがフィールド・コンストラクタ・アクセサ・equals / hashCode / toString を自動生成してくれるので、
ボイラープレートなしで“値オブジェクト”を表現できる。
状態を変えない DTO やレスポンス、設定値などには record を使い、
状態を持つオブジェクトや継承が必要なものには従来の class を使う、
という使い分けを意識すると設計がきれいになる。」

タイトルとURLをコピーしました