Java | Java 詳細・モダン文法:設計・実務視点 – イミュータブル設計

Java Java
スポンサーリンク

イミュータブル設計を一言でいうと

イミュータブル(immutable)は、「一度作ったら中身が変わらないオブジェクト」のことです。
Java で一番有名なのは String です。Stringreplacesubstring を呼んでも、自分自身は変わらず、新しい 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 は、一度作ったら nameage も変えられません。
クラス自体も 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 歳
Java

u1 は 20 歳のまま変わらず、u2 は 21 歳の別インスタンスです。
「過去の状態を保持したまま、新しい状態を作る」というスタイルは、
バグ調査や履歴管理にも向いています。


イミュータブル設計で気をつけるべきポイント

フィールドの中身もイミュータブルかどうか

フィールドがプリミティブ型や String のようなイミュータブルなら簡単ですが、
ListMap のようなミュータブルなオブジェクトをフィールドに持つ場合は注意が必要です。

例えば、次のクラスは「見かけ上イミュータブル」ですが、実は中身が変えられてしまいます。

public final class Group {
    private final List<String> members;

    public Group(List<String> members) {
        this.members = members;
    }

    public List<String> members() {
        return members;
    }
}
Java

members() が返す List に対して、呼び出し側が addremove を呼べてしまいます。
つまり、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;
    }
}
Java

Money がイミュータブルだと、
「どこかで勝手に金額が変わっていた」という事故が起きません。

DTO やレスポンスオブジェクトもイミュータブルにできる

API のレスポンスや、レイヤー間のデータ受け渡しに使う DTO も、
イミュータブルにしておくと安心です。

特に、「作ったあとに書き換える必要がないデータ」は、
積極的にイミュータブルにしてしまった方が、
後から仕様が変わったときにも壊れにくくなります。


まとめ:イミュータブル設計を自分の言葉で説明するなら

あなたの言葉で整理すると、こうなります。

「イミュータブル設計とは、『一度作ったら中身が変わらないオブジェクト』としてクラスを設計すること。
フィールドを private final にし、セッターを持たず、変更は“新しいインスタンスを返す”形で表現する。
そうすることで、状態の追跡が楽になり、マルチスレッドでも安全になり、テストもしやすくなる。

フィールドの中身(List や Map など)まで含めて本当に不変かどうかに注意しつつ、
値オブジェクトや DTO など『作ったあとに変える必要がないもの』からイミュータブルにしていくと、
実務のコードがぐっと安定して読みやすくなる。」

タイトルとURLをコピーしました