こんにちは、かつコーチです。
今回は不変オブジェクト(イミュータブルオブジェクト)の設計を扱います。final・record・equals/hashCodeの実装は理解している前提で進めます。
不変オブジェクトとは何か、なぜ必要か
不変オブジェクトの定義
不変オブジェクトとは、生成後に内部状態が一切変更できないオブジェクトのことです。
JavaのStringはその代表例で、str.concat("abc")は新しいStringインスタンスを返すだけで、元のstrは変わりません。
可変オブジェクトが引き起こす事故
筆者が実際に遭遇したバグに、Listを戻り値としてそのまま返してしまい、呼び出し側で意図せず内容を書き換えられてしまったケースがあります。
// ❌Before:内部の可変な状態をそのまま公開してしまう
class ShoppingCart {
private final List<String> items = new ArrayList<>();
void addItem(String item) {
items.add(item);
}
List<String> getItems() {
return items; // 内部のリストへの参照をそのまま返している
}
}
ShoppingCart cart = new ShoppingCart();
cart.addItem("りんご");
List<String> items = cart.getItems();
items.clear(); // カート内部のリストまで空になってしまう
getItems()が返したリストを呼び出し側でclear()すると、ShoppingCart内部のリストまで空になります。
呼び出し側は「参照を受け取っただけ」のつもりでも、実際には内部状態を直接操作できてしまうのが原因です。
// ✅After:防御的コピーで内部状態を保護する
class ShoppingCart {
private final List<String> items = new ArrayList<>();
void addItem(String item) {
items.add(item);
}
List<String> getItems() {
return List.copyOf(items); // 変更不可なコピーを返す
}
}
List.copyOf()(Java 10以降)は変更不可なリストのコピーを生成するため、呼び出し側がaddやclearを呼ぶとUnsupportedOperationExceptionが発生し、事故を早期に検知できます。
不変クラスを設計する4つのルール
不変クラスの条件
不変クラスとして安全と言えるためには、以下の4条件を満たす必要があります。
- すべてのフィールドを
finalにする - クラス自体を
finalにするか、サブクラスでの変更を防ぐ設計にする - コンストラクタで受け取った可変オブジェクトは防御的コピーを取る
- ゲッターで内部の可変フィールドをそのまま返さない
// ❌Before:一見finalフィールドだが、可変オブジェクトを外部に渡している
final class Event {
private final String name;
private final Date date; // Dateは可変オブジェクト
Event(String name, Date date) {
this.name = name;
this.date = date; // コンストラクタ引数の参照をそのまま保持
}
Date getDate() {
return date; // 内部のDateをそのまま返す
}
}
Date original = new Date();
Event event = new Event("リリース会", original);
original.setTime(0); // Eventの外からEvent内部の日付を書き換えられてしまう
event.getDate().setTime(0); // ゲッター経由でも書き換えられる
dateフィールドはfinalですが、Date自体が可変オブジェクトのため、参照を共有している限り外部から中身を書き換えられます。
// ✅After:不変な型(LocalDateTime)を使い、防御的コピーの必要をなくす
final class Event {
private final String name;
private final LocalDateTime date;
Event(String name, LocalDateTime date) {
this.name = name;
this.date = date;
}
LocalDateTime getDate() {
return date;
}
}
Java 8以降のjava.timeパッケージ(LocalDateTime・LocalDate等)はそもそも不変オブジェクトとして設計されているため、これらを使う限り防御的コピーを意識する必要がありません。
古いjava.util.Date・Calendarを避け、java.time系のクラスを使うことも不変オブジェクト設計の一部です。
recordで不変オブジェクトを簡潔に作る
recordが解決する冗長さ
Java 16で正式導入されたrecord(Java21では標準機能)は、不変なデータの入れ物を簡潔に定義できる機能です。
// ❌Before:不変クラスを手動で書くと冗長
final class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
int getX() { return x; }
int getY() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) 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 + "]";
}
}
// ✅After:recordなら1行で同等の不変クラスができる
record Point(int x, int y) {}
recordはequals・hashCode・toString・各フィールドのアクセサ(x()、y())を自動生成し、フィールドは暗黙的にfinalになります。
不変オブジェクトを作りたいだけなら、手書きのクラスよりrecordを第一候補にするべきです。
recordでも防御的コピーは必要になる場合がある
recordはフィールドをfinalにしてくれますが、フィールドの型自体が可変オブジェクトの場合は注意が必要です。
// ❌Before:recordでもList型フィールドは可変な参照を共有してしまう
record Team(String name, List<String> members) {}
List<String> memberList = new ArrayList<>(List.of("田中", "佐藤"));
Team team = new Team("開発チーム", memberList);
memberList.add("鈴木"); // Teamの外から内部のリストが変更されてしまう
// ✅After:コンパクトコンストラクタで防御的コピーを行う
record Team(String name, List<String> members) {
Team {
members = List.copyOf(members); // コンパクトコンストラクタで不変化
}
}
recordのコンパクトコンストラクタ(フィールドリストの再宣言を省略した専用コンストラクタ構文)を使うことで、受け取ったListを不変なコピーに変換し、フィールド代入前に検証・加工を挟めます。
不変オブジェクトを使うメリットと注意点
スレッドセーフティが自動的に手に入る
不変オブジェクトは状態が変化しないため、複数スレッドから同時にアクセスされても、同期処理(synchronized)なしで安全に共有できます。
並行処理を扱う設計では、可変な状態をできる限り減らし、不変オブジェクトに置き換えることが定石とされています。
注意点:大量オブジェクト生成によるメモリ負荷
状態を変更するたびに新しいインスタンスを生成するため、頻繁に更新が発生する処理では不向きな場合があります。
文字列連結でStringBuilderが推奨されるのと同じ理由で、「更新頻度が高い処理」には可変なビルダー型を併用するのが実務的な判断です。
まとめ
この記事のポイント
- 不変オブジェクトは生成後に状態変更できず、内部の可変な参照をそのまま外部に渡さない設計が必須
java.time系のような不変な型を使うことで防御的コピーの手間を減らせるrecordは不変クラスを簡潔に定義でき、可変フィールドはコンパクトコンストラクタで防御的コピーする- 不変オブジェクトはスレッドセーフだが、更新頻度が高い処理には向かない場合がある
次に読むべき記事
- 代表的デザインパターン(Singleton・Factory・Strategy)の記事へ
- record(Java21)でシンプルにデータを表現するの記事へ
タグ: Java, 上級者向け, 設計