1. DTO クラスの問題: なぜ record クラスが必要か?
正直に言いましょう。こんなクラスを何回書いたことがありますか?
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{" + "x=" + x + ", y=" + y + '}';
}
}
難しくはなさそうですが、行数を数えてみてください! そして同様のクラスが 10 個あり、それぞれに 5–6 個のフィールドがあると想像してみましょう。IDE でさえこのボイラープレート生成に疲れます。新しいフィールドを追加しようものなら、コンストラクタ、ゲッター、equals、hashCode、toString をすべて直す羽目に……。退屈で面倒、しかもバグの温床です。
この手のクラスは DTO(Data Transfer Object)や Value Object と呼ばれます。 単にデータを保持するだけ——それだけです。しかし、ボイラープレートのせいで保守が大変になります。
「大した問題じゃない」と思うなら、50 個の同種クラスを一斉に変更する時まで待ってみてください。そのとき、record クラスのありがたみをきっと痛感します!
2. record 入門: 構文と Java 16+ のマジック
Java 16 で状況は一変しました。新しい種類のクラス record が登場したのです。データの集合をただ保持したいケースのために設計されています。構文は他言語のタプルに近い感覚です。
record の宣言方法
public record Point(int x, int y) { }
これでおしまい! 今、2 つのフィールド、コンストラクタ、アクセサ、equals、hashCode、toString を持つ不変クラスができました。余計な記述は不要です。
Java は「内部で」何をしているのか?
- フィールド x と y は private かつ final になります。
- アクセサ は自動生成されます: int x() と int y()。
- コンストラクタ: public Point(int x, int y)。
- equals/hashCode: すべてのフィールドを値で比較します。
- toString: "Point[x=1, y=2]" のような文字列を返します。
言ってみれば、record は「強化版 DTO」です。コードは少なく、保証は多く、バグは減ります。
不変(immutability)
record クラスのすべてのフィールドは自動的に final になります。オブジェクト生成後にそれを変更することはできず、そのことはコンパイラが保証します。
セッターを追加したり、フィールドを final でなくしようとすれば、コンパイラが止めてくれます。ここまで面倒を見てくれるのは珍しいことです!
3. record クラスの使用例
従来のクラス(コードが多い):
public class Client {
private final String name;
private final int id;
public Client(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() { return name; }
public int getId() { return id; }
// equals, hashCode, toString ...
}
Record クラス(1 行!):
public record Client(String name, int id) { }
使用例:
public class Main {
public static void main(String[] args) {
Client client = new Client("イワン", 123);
System.out.println(client.name()); // イワン
System.out.println(client.id()); // 123
System.out.println(client); // Client[name=イワン, id=123]
}
}
注意:
- フィールドへのアクセサメソッドはフィールド名と同じ: name()、id()。
- setName() や setId() はありません——生成後にオブジェクトを変更できないためです。
4. record クラスの利点: コードもバグも減らす
コードが少ない = 幸せが増える
40 行書く必要があるなら、1 行で済ませましょう。record クラスは時間と労力を節約します。特に DTO や value オブジェクトが多い大規模プロジェクトで威力を発揮します。
「契約として」の不変性
- record クラスは常に final で、不変です。
- オブジェクトを改ざん・誤変更しにくくなります。
- 意図しない箇所で状態が変わることによる「奇妙な」バグがなくなります。
- (すべてのフィールドも不変であれば)マルチスレッドでも安全に使えます。
equals/hashCode/toString の自動生成
比較、ハッシュ計算、見やすい出力のメソッドを手書きする必要はありません。すべて正しく自動生成されます。
Client c1 = new Client("アンナ", 42);
Client c2 = new Client("アンナ", 42);
System.out.println(c1.equals(c2)); // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1); // Client[name=アンナ, id=42]
コレクションやキーに最適
record オブジェクトは HashMap のキー、HashSet の要素などとして利用できます。equals と hashCode が全フィールドを考慮するため、正しく動作します。
import java.util.HashMap;
import java.util.Map;
Map<Client, String> clients = new HashMap<>();
clients.put(new Client("アンナ", 42), "VIP");
System.out.println(clients.get(new Client("アンナ", 42))); // VIP
データの明示的な記述
record クラスの構文を見れば、どんなデータを保持し、オブジェクトが不変であるかがひと目でわかります。これは他の開発者(そして半年後の自分)にとってコードを理解しやすくします。
5. 表: 従来のクラスと record クラスの比較
| 従来のクラス | Record クラス | |
|---|---|---|
| 構文 | コードが多い | 1 行で定義 |
| 不変性 | 明示的な実装が必要 | コンパイラが保証 |
| メソッドの自動生成 | なし | あり(equals, hashCode, toString) |
| フィールド追加 | 可能 | record のコンポーネントのみ |
| 継承 | 継承可能 | 常に final。継承不可 |
| コレクションでの利用 | メソッドを正しく実装する必要 | 標準でそのまま動作 |
6. 学習用アプリを拡張: record クラスの例
学習用の銀行アプリで、口座の取引(日時、金額、取引タイプ(例: 「入金」「出金」))を保持するとしましょう。
Java 16 以前:
public class Transaction {
private final LocalDate date;
private final double amount;
private final String type;
public Transaction(LocalDate date, double amount, String type) {
this.date = date;
this.amount = amount;
this.type = type;
}
public LocalDate getDate() { return date; }
public double getAmount() { return amount; }
public String getType() { return type; }
// equals, hashCode, toString ...
}
Java 16 と record の場合:
import java.time.LocalDate;
public record Transaction(LocalDate date, double amount, String type) { }
使用例:
Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t); // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0
7. 可視化: record が生成するもの
「展開した」record クラス(コンパイラが概ね生成するもの)は次のようになります。
public final class Point extends java.lang.Record {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) { /* フィールドによる比較 */ }
@Override
public int hashCode() { /* フィールドに基づく計算 */ }
@Override
public String toString() { /* 見やすい出力 */ }
}
まとめ: record を使うべきとき
- 不変なデータの集合を持つオブジェクトが必要なとき。
- 手書きなしで「正しい」equals/hashCode/toString が必要なとき。
- DTO、Value Object、ペアやタプル、色、点、範囲、コレクションのキーなどを作るとき。
8. record クラスでありがちな誤り
エラー No.1: セッターを追加したり、生成後にフィールドを変更しようとする。
record クラスは自分のフィールドを変更できません。setX(int x) のようなメソッドを追加しようとすると、コンパイラがすぐに拒否します。フィールドを直接変更しようとしても同様です。
エラー No.2: 非 static フィールドを追加しようとする。
record クラスで宣言できるのはコンポーネント(record 名の後の丸括弧内に書くフィールド)と static フィールドだけです。通常の非 static フィールドは追加できず、コンパイラが許しません。
エラー No.3: mutable なロジックに record を使う。
record クラスは可変状態を持つオブジェクト向けではありません。生成後に状態を変える必要があるなら、従来のクラスを使いましょう。
エラー No.4: record は常に final であることを忘れる。
record クラスは継承できず、かつスーパークラスにもできません。この制約に反するとコンパイルエラーになります。重要なポイントは、record を「拡張」しようとしないことです。record は完結した不変型として設計されています。
エラー No.5: 自動生成メソッドを軽視する。
equals、hashCode、toString をオーバーライドする場合は要注意です——その契約を破らないようにしないと、コレクションや比較が正しく動かなくなります。
GO TO FULL VERSION