イミュータブル設計を一言でいうと
イミュータブル(immutable)は、「一度作ったら中身が変わらないオブジェクト」のことです。
Java で一番有名なのは String です。String は replace や substring を呼んでも、自分自身は変わらず、新しい String を返します。
イミュータブル設計とは、「状態を持つクラスを、できるだけこの String のように“作ったら二度と変わらない”形で設計しよう」という考え方です。
特に実務では、マルチスレッドやバグの少なさ、テストのしやすさに直結する、とても強力な武器になります。
なぜイミュータブルがそんなに大事なのか
状態が変わらないと「考えること」が一気に減る
ミュータブル(可変)なオブジェクトは、「いつ」「どこで」「誰が」その状態を変えるかを常に意識しなければなりません。
例えば、次のようなクラスを考えます。
class User {
String name;
int age;
}
Javaこの User のインスタンスを、あちこちのメソッドに渡して、
どこかで user.age++ されたり、user.name = "???"; されたりすると、
「今この瞬間の user の状態」がとても追いにくくなります。
一方、イミュータブルなら、「一度作ったら絶対に変わらない」と分かっているので、
「このオブジェクトはいつ見ても同じ状態」という前提で考えられます。
これは、コードを読むときの負担をものすごく減らしてくれます。
マルチスレッドで「ロック地獄」から解放される
複数スレッドから同じオブジェクトにアクセスするとき、
そのオブジェクトがミュータブルだと、
「同時に書き換えられたら壊れる」ので、ロックや同期が必要になります。
しかし、イミュータブルなオブジェクトは「そもそも書き換えられない」ので、
何スレッドから同時に読まれても安全です。
つまり、イミュータブル設計は、
「スレッドセーフをほぼ自動で手に入れる」ための強力な手段でもあります。
Java でイミュータブルクラスを作る基本パターン
フィールドはすべて private final
まず、フィールドはすべて private final にします。final にすることで、「コンストラクタで一度だけ代入され、その後は変えられない」ことが保証されます。
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;
}
}
Javaこの User は、一度作ったら name も age も変えられません。
クラス自体も final にしておくと、サブクラスで勝手にミュータブルにされる心配も減ります。
セッターを作らない、「変更」は新しいインスタンスで表現する
ミュータブルなクラスだと、よくこんなメソッドを書きます。
user.setAge(user.getAge() + 1);
Javaイミュータブルなクラスでは、セッターは作りません。
代わりに、「変更された状態を持つ新しいインスタンスを返す」メソッドを用意します。
public User withAge(int newAge) {
return new User(this.name, newAge);
}
Java使う側はこうなります。
User u1 = new User("Alice", 20);
User u2 = u1.withAge(21); // u1 はそのまま、u2 は 21 歳
Javau1 は 20 歳のまま変わらず、u2 は 21 歳の別インスタンスです。
「過去の状態を保持したまま、新しい状態を作る」というスタイルは、
バグ調査や履歴管理にも向いています。
イミュータブル設計で気をつけるべきポイント
フィールドの中身もイミュータブルかどうか
フィールドがプリミティブ型や String のようなイミュータブルなら簡単ですが、List や Map のようなミュータブルなオブジェクトをフィールドに持つ場合は注意が必要です。
例えば、次のクラスは「見かけ上イミュータブル」ですが、実は中身が変えられてしまいます。
public final class Group {
private final List<String> members;
public Group(List<String> members) {
this.members = members;
}
public List<String> members() {
return members;
}
}
Javamembers() が返す List に対して、呼び出し側が add や remove を呼べてしまいます。
つまり、Group の内部状態が外から変えられてしまうわけです。
これを防ぐには、コンストラクタとゲッターで「防御的コピー」や「不変ビュー」を使います。
public final class Group {
private final List<String> members;
public Group(List<String> members) {
this.members = List.copyOf(members); // コピーして保持
}
public List<String> members() {
return List.copyOf(members); // あるいは Collections.unmodifiableList(...)
}
}
Javaこうすると、外から members をいじることはできません。
「フィールドが指している先もイミュータブルか?」を必ず意識してください。
パフォーマンスとのバランス
イミュータブルは安全ですが、「毎回新しいインスタンスを作る」という性質上、
オブジェクト生成が増えることがあります。
ただし、ここで大事なのは、
「まずは正しさ・分かりやすさを優先し、必要になったら局所的に最適化する」
という順番です。
イミュータブル設計で得られるメリット(バグの減少、スレッドセーフ、テストのしやすさ)は非常に大きく、
多くの場合、多少のオブジェクト生成コストを上回ります。
実務でのイミュータブル設計の使いどころ
値オブジェクト(Value Object)をイミュータブルにする
ドメイン駆動設計などで出てくる「値オブジェクト」は、
イミュータブルにするのが定石です。
例えば、金額やメールアドレス、ユーザーID、期間など、
「同じ値なら同じ意味を持つ」ものは、イミュータブルにすると扱いやすくなります。
public final class Money {
private final long amount;
public Money(long amount) {
if (amount < 0) throw new IllegalArgumentException();
this.amount = amount;
}
public Money add(Money other) {
return new Money(this.amount + other.amount);
}
public long amount() {
return amount;
}
}
JavaMoney がイミュータブルだと、
「どこかで勝手に金額が変わっていた」という事故が起きません。
DTO やレスポンスオブジェクトもイミュータブルにできる
API のレスポンスや、レイヤー間のデータ受け渡しに使う DTO も、
イミュータブルにしておくと安心です。
特に、「作ったあとに書き換える必要がないデータ」は、
積極的にイミュータブルにしてしまった方が、
後から仕様が変わったときにも壊れにくくなります。
まとめ:イミュータブル設計を自分の言葉で説明するなら
あなたの言葉で整理すると、こうなります。
「イミュータブル設計とは、『一度作ったら中身が変わらないオブジェクト』としてクラスを設計すること。
フィールドを private final にし、セッターを持たず、変更は“新しいインスタンスを返す”形で表現する。
そうすることで、状態の追跡が楽になり、マルチスレッドでも安全になり、テストもしやすくなる。
フィールドの中身(List や Map など)まで含めて本当に不変かどうかに注意しつつ、
値オブジェクトや DTO など『作ったあとに変える必要がないもの』からイミュータブルにしていくと、
実務のコードがぐっと安定して読みやすくなる。」

