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]
Javarecord の 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 を使う、
という使い分けを意識すると設計がきれいになる。」
