1. record クラスと不変性
不変性とは、オブジェクトの状態が生成後に変更できない性質のことです。言い換えると、オブジェクトを一度作成したら、その内部データを変更することはできません。以上。石に刻まれたように固定されます。
電車のチケットを思い浮かべてください。いったん印刷されたら、出発日や発車場所を変更できません(フォトショップでの小細工は別として)。チケットは不変オブジェクトです。別のチケットが必要なら、新しいものを購入します。
プログラミングでは、このようなオブジェクトを immutable objects と呼びます。偶発的な変更からプログラムを守り、コードをより安全で予測可能にします。
不変オブジェクトの特徴
- オブジェクトのすべてのフィールドは final(通常はコンストラクタで一度だけ代入されます)。
- セッター(フィールドを変更するメソッド)がない。
- 内部データを返すメソッドは、コピーを返すか、もとのデータ自体が不変である。
Java の record クラスは、不変なデータ構造を簡単に、そして痛みなく作るために設計されました。
なぜ record は不変なのか?
- record の全コンポーネントは final。
record を宣言すると、コンパイラはそのすべてのフィールドを自動的に private かつ final にします。つまり、オブジェクトを作成した後にフィールドを変更することはできません。 - セッターがない。
record クラスに setX(int x) のようなメソッドを追加して、生成後にフィールドの値を変更することはできません。コンパイラが許しません。 - コンストラクタは一度だけ代入する。
すべての値はオブジェクトの生成時にのみ設定されます。
例
public record Point(int x, int y) {}
Point p = new Point(5, 10);
// p.x = 7; // コンパイルエラー: フィールド x は private かつ final です
// p.x(7); // エラー: セッターはありません!
System.out.println(p.x()); // 5
フィールドを変更したり、存在しないセッターを呼び出そうとするとコンパイルエラーになります。Java は不変性の契約を厳格に守らせます。
2. 不変オブジェクトの利点
マルチスレッドでの安全性
マルチスレッドのプログラム(いまや大半がそうです)では、不変オブジェクトは防弾チョッキのようなものです。オブジェクトが変更できなければ、複数のスレッドが安心して読み取れ、誰かが同時にデータを変更する心配がありません。アクセスの同期を行ったりデータレースを気にしたりする必要がなくなります。
事実: Java 標準ライブラリの中でマルチスレッドなシナリオでよく使われる多くのクラスは、不変であるか、または変更から特別に保護されています。
コード理解の容易さ
オブジェクトが不変であれば、別のメソッドやクラスに渡しても「背後で」変わらないと常に確信できます。これによりコードの読みやすさやデバッグが大幅に楽になります。誰がどこでフィールドを変えたのか推測する必要はありません——誰も変えられないのです。
コレクションのキーに適している
不変オブジェクトは、HashMap や HashSet のようなコレクションのキーとして非常に適しています。なぜなら、その equals と hashCode は変更されないフィールドだけに依存するからです。つまり、状態が変わってコレクション内で「見失う」ことがありません。
隠れたバグが減る
可変オブジェクトは、参照をうっかり別の場所に渡すだけで簡単に壊れてしまいます。一方で不変オブジェクトは印刷された本のようなものです。誰もページを破ったり書き換えたりできません。
3. 通常のクラスとの比較
通常のクラスと record クラスの振る舞いを比較してみましょう。例として、平面上の点の簡単なモデルを取り上げます。
通常のクラス(mutable)
public class PointClass {
private int x;
private int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
オブジェクトを作ってから、いくらでもその状態を変更できます:
PointClass p = new PointClass(1, 2);
p.setX(10); // これで p.x は 10
record クラス(immutable)
public record Point(int x, int y) {}
インスタンスを作ったら、それは生成時のまま固定されます:
Point p = new Point(1, 2);
// p.x = 10; // エラー! フィールドにはアクセスできません
// p.x(10); // エラー! セッターはありません
表: 振る舞いの比較
| 通常のクラス | record クラス | |
|---|---|---|
| フィールド | 任意 | private final のみ |
| セッター | 追加できる | 追加できない |
| 可変性 | 可変(mutable) | 不変 |
| 自動生成 | いいえ | はい (equals, hashCode, toString) |
4. 実践: record クラスの不変性をどう使うか
小さな例で record クラスを使ってみましょう。銀行アプリがあり、トランザクションの情報を保存したいとします:
public record Transaction(String fromAccount, String toAccount, double amount) {}
オブジェクトを生成:
Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]
お金を別口座へ「送金」してみましょう:
// t.toAccount = "11111"; // エラー! フィールドは final で、アクセスできません
// t.toAccount("11111"); // エラー! セッターはありません
別のトランザクションが必要なら、新しいオブジェクトを作成します:
Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());
重要: 不変だからといって「不便」という意味ではありません。必要な状態が変わるなら新しいオブジェクトを作る——ただそれだけのスタイルです。
5. 不変性の注意点: 覚えておくべきこと
不変性は必ずしも絶対ではない!
record クラスは、そのフィールドが変化しないことを保証します。しかし、フィールドが可変オブジェクト(たとえば配列や通常のクラス)への参照である場合、そのオブジェクトの中身は変更可能です。
配列の例
public record DataHolder(int[] data) {}
int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99! 配列の中身が変わりました
結論: 真に不変にしたいなら、String、Integer、他の record などの不変型だけを使うか、record クラスのカノニカルコンストラクタ内で可変オブジェクトをコピーしてください。例えば:
int[] copy = Arrays.copyOf(data, data.length);
6. 通常のクラスを自前で不変にするには
通常のクラスを immutable(不変)にしたい場合は、手作業で次を行います:
- すべてのフィールドに private と final を付ける。
- セッターを追加しない。
- すべてのフィールドをコンストラクタ経由のみで初期化する。
- フィールドが可変オブジェクトなら、防御的コピー(defensive copy)を行う。
例
public final 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;
}
}
record クラスなら、もっと簡潔に書けます:
public record User(String name, int age) {}
7. 不変な record クラスでのよくある誤り
誤り №1: 生成後にフィールドを変更しようとする。
初心者はよく、record オブジェクトに対して p.x = 42; や p.x(42); と書こうとします。しかしコンパイラはすぐに言います: 「それはできません!フィールドは final で、セッターはありません」。
誤り №2: 可変オブジェクトを record のコンポーネントとして使う。
record に List、Map、配列、その他の可変オブジェクト型のフィールドを追加した場合、record そのものはそのオブジェクトの中身の変更からは守ってくれません。例えば record User(List<String> hobbies) があると、誰かがリストに要素を追加・削除でき、それによって record オブジェクトの状態が変わってしまいます。これを避けるには、不変コレクション(List.copyOf、Collections.unmodifiableList)を使うか、record のコンストラクタ内でコレクションのコピーを作成してください。
誤り №3: 不変性の誤解。
オブジェクトが record であれば、あらゆる変更から守られていると考えてしまう人がいます。しかし実際には、フィールドが可変オブジェクトへの参照であれば、その中身は変更でき、予期しないバグにつながります。
GO TO FULL VERSION