1. record と class の比較: 主な違いは何か?
Java ではデータ型を定義する主な方法が 2 つあります。通常のクラス(class)と record クラス(record)です。一見どちらもデータの保持と処理ができますが、少し掘り下げると違いは想像以上に多いのです。
違いの表: class vs record
| 特徴 | 通常のクラス (class) | record クラス (record) |
|---|---|---|
| 可変性 | 任意: フィールドを final にしても、しなくてもよい | 不変: すべてのフィールドが final |
| 継承 | 継承可能(extends)。既定では final ではない | 常に final。スーパークラスにはなれない |
| フィールド | 任意: static/非static、final/非final、任意の型 | record のコンポーネントのみ(private final)、加えて static フィールド |
| ゲッター/セッター | 自分で実装(または Lombok で生成) | ゲッターは自動生成(メソッド名はフィールド名と同じ)、セッターはない |
| equals/hashCode/toString | 通常は手書き/生成(equals、hashCode、toString) | 全コンポーネントに基づき自動生成 |
| コンストラクタ | 任意、いくつでも定義可能 | 主要コンストラクタが 1 つ(全コンポーネントを受け取る)。コンパクトコンストラクタを追加可能 |
| インターフェース | 実装可能 | 実装可能 |
| 追加メソッド | 任意に追加可能 | 追加可能だがメソッドのみ(フィールドは不可) |
| コレクションでの利用 | 可能。ただし equals/hashCode を正しく実装する必要あり | キー/値に理想的。必要な実装は既に提供済み |
分かりやすい例
通常のクラス:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
record クラス:
public record Person(String name, int age) { }
以上です! 1 行のコードで同等(それ以上)のものが得られます。しかも、重要な実装をうっかり書き忘れるリスクもありません。
2. record クラスの制約
record クラスは単なる「短い構文」ではなく、独立した概念であり、厳密な規則があります。詳しく見ていきましょう。
record は常に final
record クラスは定義上いつでも final です。つまり、record からサブクラスを作成することはできません:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // コンパイルエラー!
振る舞いを拡張したい場合は、通常のクラスやコンポジション(record をクラスに内包)を使いましょう。
record はスーパークラスになれない
record クラスは他のクラスの親にはなれず、常に final です。これは当然です。もし可能だと、誰かが可変フィールドを追加できてしまい、「不変データ」というコンセプトが崩れてしまいます。
final フィールド(コンポーネント)のみ
record のコンポーネントはヘッダで宣言され、既定で private final です。record の本体にインスタンス(非static)フィールドを追加することはできません:
public record User(String login, String email) {
// int counter; // エラー! 非static フィールドは追加できない
static int totalUsers = 0; // これは許可される(static フィールド)
}
セッターはない
record クラスにはコンポーネント用のセッターを持てません。たとえば setX(int x) のようなメソッドを追加しても意味がありません。オブジェクト生成後に値を変更することはできないからです。
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // エラー: final フィールドは変更できない
}
引数なしコンストラクタはない
record クラスには常に、すべてのコンポーネントを受け取る主コンストラクタしかありません。すべての値を指定せずに record を作ることはできません:
Point p = new Point(1, 2); // OK
// Point p = new Point(); // エラー: 引数なしのコンストラクタはない
インスタンス初期化子は使えない
record クラスでは、メソッド外のインスタンス初期化子(ブロック)は使用できません:
public record User(String login) {
// { /* ... */ } // エラー: インスタンス初期化子は禁止
}
継承に関する制約
record クラスは他のクラスを明示的に継承できません(java.lang.Record は、すべての record の基底クラスとして暗黙に継承されます)。一方で、インターフェースの実装は可能です。
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("本を印刷: " + title);
}
}
複雑なビジネスロジックには不向き
record はデータ志向であり、振る舞いを重視するものではありません。オブジェクトに複雑なロジック、可変状態、「ライフサイクル」や多数の依存があるなら、record は適しません。通常のクラスを使いましょう。
3. record クラスを使うべき場面
- DTO (Data Transfer Object): アプリケーションの層間、サービス間、マイクロサービスや REST コントローラ間で不変データを受け渡す用途(例: JSON レスポンス)。
- Value Object: 値のみで同一性が決まるオブジェクト。
- コレクションのキーや値: equals と hashCode の正しい実装が重要な場合(例: HashMap や Set での利用)。
- 計算結果: メソッドから複数の値をまとめて返したい場合(例: record Pair<T, U>(T first, U second))。
例: REST コントローラ用の DTO
public record UserDto(String login, String email) { }
これで、誰かにフィールドを変更される心配なく、この型のオブジェクトをコントローラから安心して返せます。
例: HashMap のキー
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// すべて正しく動作します: equals と hashCode は既に実装済み!
4. record クラスを使うべきでない場面
- 可変な状態: オブジェクト生成後に少なくとも 1 つのフィールドを変更する必要がある場合。
- 複雑なロジック: 多くのメソッドや、可変状態を持つ入れ子のオブジェクトがある場合。
- 継承: クラス階層や抽象基底クラス、メソッドのオーバーライドが必要な場合。
- ビジネスエンティティ: 例として、データベース上に存在し固有 ID を持つオブジェクトなど。
例: 通常のクラスが必要な場合
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
このようにオブジェクトの状態が変化するのは明らかなので、record には不向きです。
5. 実践例: record と class の選び方
例 1: record が最適
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- この長方形は幅と高さだけで定義されます。
- 生成後にこれらの値を変更する必要はありません。
- 有用なメソッド area() を追加できます。
- その他は Java が面倒を見てくれます。
例 2: class が最適
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
生成後に長方形のサイズを変更したいですか? 通常のクラスを使いましょう。
6. record クラスでよくあるミス
エラー №1: インスタンス(非static)フィールドを追加しようとする。
record クラスでは、コンポーネント一覧の外側でインスタンスフィールドを宣言できません。そうしようとするとコンパイルエラーになります。例:
public record City(String name) {
// int population; // エラー!
}
エラー №2: セッターを追加したくなる。
record はコンポーネントのセッターをサポートしません。オブジェクト生成後にフィールド値を変更しようとすると、コンパイルエラーになります。
エラー №3: record を継承する/record から継承させる。
record は常に final です。record を継承したり、record が他のクラス(暗黙の java.lang.Record を除く)を継承することはできません。
エラー №4: 可変オブジェクトに record を使う。
生成後にオブジェクトの状態を変更する予定があるなら、record は向きません! 通常のクラスを使いましょう。
エラー №5: コンストラクタの制約を忘れる。
record クラスには、すべてのコンポーネントを受け取るコンストラクタが必須です。引数なしコンストラクタはありません。
GO TO FULL VERSION