【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) {}
生成されるもの(一部)
- 不変フィールド
nameとage - アクセッサ
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 をデフォルトで検討しよう
参考リンク
- Oracle Docs — Record Classes
- JEP 395: Records
- その他ブログ・StackOverflow など
以上、Java のレコード機能紹介でした! 感想や質問はコメント欄へどうぞ 🙌
Discussion