📼

【Java】レコードとは?面倒なオーバーライドがいらない!?

に公開

【Java】レコードとは?面倒なオーバーライドがいらない!?

はじめに

こんにちは!インターンシップに向けて 『スッキリわかる Java 入門 実践編 第4版』 で Java を勉強中の筆者です。Python や PHP といった“ゆるふわ”言語に慣れていたせいか、Java には「クラスを作ったら equals / hashCode / toString を必ず書こう」という暗黙知がありビックリしました。

しかし Java 16 から正式リリースされた Record(レコード) 機能を使うと、これらのメソッドを自分で実装せずに済むケースが大幅に増えます。今回はその魅力と注意点をまとめました!


前提:なぜ equals / hashCode / toString を書くの?

Java のクラスは、値オブジェクトとして扱う際に 以下 3 つのメソッドのオーバーライドが強く推奨 されます。

メソッド 主な役割
equals(Object o) 論理的な同値性を定義する
hashCode() HashMap / HashSet などハッシュ系コレクションで使うキーを生成する
toString() ログやデバッグ時に人間が読める文字列を返す

書かないとどうなる?

class Person {
    String name;
    int age;
    Person(String name, int age) {
        this.name = name;
        this.age  = age;
    }
    // equals/hashCode/toString は未実装!
}

Set<Person> people = new HashSet<>();
people.add(new Person("Alice", 25));
people.add(new Person("Alice", 25));
System.out.println(people.size()); // => 2 (重複を検知できない)
System.out.println(people.iterator().next()); // => Person@6f496d9f (読みづらい)

equals()hashCode() が未実装のため、内容が同じでも別オブジェクト扱いtoString() も “クラス名@ハッシュ値” 形式で味気ない表示になります。


一方レコードでは……

レコードは「データキャリア 専用に最適化されたクラス宣言」です。次の 1 行を書くだけで、上記 3 メソッドを含む定型コードが自動生成されます。

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

生成されるもの(一部)

  • 不変フィールド nameage
  • アクセッサ name() / age()get は付かない!)
  • コンパクトコンストラクタ(オプションでバリデーション可能)
  • equals() / hashCode() / toString()

"レコードヘッダだけで状態を完全に表し、その状態に基づく基本メソッドをコンパイラが合成する" という設計思想により、ボイラープレートが激減します。


注意点(必ず読もう)

以下の例では わざと誤った書き方 を示し、なぜコンパイルできない/動かないかを解説します。

1. レコード本体にインスタンスフィールドは宣言できない

public record BadRecord(String name) {
    private int age; // ❌ コンパイルエラー: レコードではインスタンスフィールドを宣言できない
}

理由: レコードの状態はヘッダのコンポーネントだけで完結させる必要があります。

2. アクセッサ名に get を付けてしまう

public record User(String email) {
    public String getEmail() { // ❌ 冗長。JavaBeans 規約にも合わない
        return email;
    }
}
System.out.println(new User("a@example.com").getEmail()); // 動くがスタイルが崩れる

理由: 自動生成される email() を使うのが正しい。カスタム getter を追加すると一貫性が失われます。

3. コンストラクタでフィールドを書き換える

public record Point(int x, int y) {
    public Point {
        x = Math.abs(x); // ✔ バリデーション用途は OK
        y = Math.abs(y);
        x++;             // ❌ コンパイルエラー: 再代入は不可 (コンポーネントは暗黙に final)
    }
}

理由: コンポーネントは final 扱い。初期化後の再代入はできません。

4. 継承しようとする

public record Employee(String name) {}

public record Manager(String name, int grade) extends Employee { } // ❌ コンパイルエラー: record は final

理由: レコードは暗黙に final。スーパークラスにもサブクラスにもできません(インタフェース実装のみ可)。

5. ビジネスロジックを持ちすぎる

public record Order(int id, List<Item> items) {
    public BigDecimal calculateTotal() { // ❌ 可能だがアンチパターン
        // 重い計算処理…
    }
}

理由: レコードはデータキャリアに留め、複雑な処理はサービスクラスなど別責務に切り出す方が保守的です。


どんな時に使う?

  • DTO(Data Transfer Object)API のリクエスト/レスポンスモデル
  • 設定ファイル・JSON のバインド先
  • データベースの 1 行 を表すイミュータブルオブジェクト(JDBC / JOOQ など)
  • ビジネスロジックを持たない 値オブジェクト

要するに「値の集まりを安全・簡潔に運びたいとき」に真価を発揮します。


こんな時は使わないで!

  • 後からフィールドを変更したい → レコードは基本イミュータブル。
  • 深い継承・ポリモーフィズムが必要 → レコードは final
  • JPA/Hibernate のエンティティ → デフォルトコンストラクタ必須& Lazy Loading などが相性悪い。
  • ビジネスロジックを多く含む → 普通のクラスで責務分割を。

まとめ

面倒なオーバーライドとはおさらば! レコードを使えば 宣言 1 行で安全・高速な値オブジェクト が完成します。まずは DTO や設定クラスから置き換えてみてください。

ボイラープレート削減で生産性 UP! 今後のコードは Record をデフォルトで検討しよう


参考リンク


以上、Java のレコード機能紹介でした! 感想や質問はコメント欄へどうぞ 🙌

Discussion