Java 逆引き集 | 不変オブジェクトの作り方(Immutable) — スレッド安全

Java Java
スポンサーリンク

不変オブジェクトの作り方(Immutable) — スレッド安全

「一度作ったら状態が変わらない」オブジェクトが不変(Immutable)。予期せぬ書き換えが起きないので、並行処理でも扱いやすく、コードの予測可能性が上がります。初心者でも確実に作れる手順とコード例をまとめます。


基本原則(これだけ守れば安心)

  • コンストラクタで完全初期化: 生成時に必要な値をすべて確定する。
  • フィールドは private final: 外部から触れさせず、再代入も不可にする。
  • 変更メソッドを持たない: setter を作らない。状態を変える代わりに「新しいインスタンス」を返す。
  • 可変オブジェクトは防御的コピー: List、Date、配列などは受け取り時・返却時にコピーする。
  • 継承を禁止(必要なら): クラスを final にして、サブクラス経由の不変性破壊を防ぐ。

最小例(値オブジェクトの定番)

public final class Money {
    private final int amount;      // 金額(整数)
    private final String currency; // 通貨コード例: "JPY"

    public Money(int amount, String currency) {
        if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");
        this.amount = amount;
        this.currency = currency;
    }

    public int amount() { return amount; }
    public String currency() { return currency; }

    // 状態変更はせず、新しいインスタンスを返す
    public Money add(int delta) {
        return new Money(this.amount + delta, this.currency);
    }

    @Override public String toString() { return amount + " " + currency; }
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money m)) return false;
        return amount == m.amount && currency.equals(m.currency);
    }
    @Override public int hashCode() { return 31 * amount + currency.hashCode(); }
}
Java
  • ポイント: setter がない/フィールドは private final/操作は新インスタンスを返す。

可変要素を含む場合の防御的コピー

受け取り時も返却時も「別物」にする

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class Order {
    private final String id;
    private final List<String> items; // 可変なので注意

    public Order(String id, List<String> items) {
        if (id == null || items == null) throw new IllegalArgumentException();
        // 受け取り時にコピー+不変ビュー
        this.id = id;
        this.items = Collections.unmodifiableList(new ArrayList<>(items));
    }

    public String id() { return id; }

    // 返すときも不変ビュー(ここでは内部をそのまま返しても不変ビューなので安全)
    public List<String> items() { return items; }

    // 追加は「状態変更」でなく、新しい Order を返す
    public Order addItem(String item) {
        List<String> copy = new ArrayList<>(items);
        copy.add(item);
        return new Order(id, copy);
    }
}
Java
  • ダメな例: 引数の List をそのまま保持して返す(外から変更されてしまう)。必ずコピー+不変化。

配列や日時の扱い(よくある落とし穴)

import java.util.Arrays;

public final class Profile {
    private final byte[] avatar; // バイナリ画像など

    public Profile(byte[] avatar) {
        if (avatar == null) throw new IllegalArgumentException();
        // 受け取り時コピー
        this.avatar = Arrays.copyOf(avatar, avatar.length);
    }

    public byte[] avatar() {
        // 返却時もコピー
        return Arrays.copyOf(avatar, avatar.length);
    }
}
Java
  • 配列は可変: 受け取り時・返却時に常にコピー。
  • 日時は java.time を推奨: LocalDate/Instant は不変なので扱いが楽。

record を使った簡潔な不変クラス(Java 16+)

public record Point(int x, int y) {
    // 追加ロジックはコンパクトコンストラクタで
    public Point {
        // 例: 不変条件チェック
        // if (x < 0 || y < 0) throw new IllegalArgumentException();
    }

    public Point move(int dx, int dy) {
        return new Point(x + dx, y + dy);
    }
}
Java
  • record は自動で不変フィールド・アクセサ・equals/hashCode/toString を生成。
  • 防御的コピーが必要な型を含むときは、コンストラクタで自前コピーを入れる。

例題で練習

例題1: 不変なメールアドレスクラス

public final class Email {
    private final String value;

    public Email(String value) {
        if (value == null || !value.contains("@")) throw new IllegalArgumentException("invalid email");
        this.value = value;
    }

    public String value() { return value; }

    // ドメイン変更は新インスタンス
    public Email withDomain(String newDomain) {
        String local = value.substring(0, value.indexOf('@'));
        return new Email(local + "@" + newDomain);
    }
}
Java

例題2: 不変な期間(開始・終了)と検証

import java.time.LocalDate;

public final class PeriodRange {
    private final LocalDate start;
    private final LocalDate end;

    public PeriodRange(LocalDate start, LocalDate end) {
        if (start == null || end == null || end.isBefore(start)) throw new IllegalArgumentException();
        this.start = start;
        this.end = end;
    }

    public LocalDate start() { return start; }
    public LocalDate end()   { return end; }

    public boolean contains(LocalDate d) {
        return (d.isEqual(start) || d.isAfter(start)) && (d.isEqual(end) || d.isBefore(end));
    }
}
Java

例題3: 不変な設定クラス(Builder併用)

import java.util.Objects;

public final class AppConfig {
    private final String url;
    private final int timeoutMs;

    private AppConfig(String url, int timeoutMs) {
        this.url = Objects.requireNonNull(url);
        this.timeoutMs = timeoutMs;
    }

    public String url() { return url; }
    public int timeoutMs() { return timeoutMs; }

    public static class Builder {
        private String url;
        private int timeoutMs = 5000;

        public Builder url(String url) { this.url = url; return this; }
        public Builder timeoutMs(int ms) { this.timeoutMs = ms; return this; }
        public AppConfig build() { return new AppConfig(url, timeoutMs); }
    }
}
Java
  • Builder は「組み立て用」。完成した AppConfig は不変。

すぐ使えるテンプレート

  • 不変クラスの基本形
public final class Value {
    private final 型 a;
    private final 型 b;

    public Value( a,  b) {
        // 必要なら検証
        this.a = a;
        this.b = b;
    }

    publica() { return a; }
    publicb() { return b; }

    // 操作は新インスタンスを返す
    public Value withA( newA) { return new Value(newA, b); }
}
Java
  • 防御的コピー(List/配列)
this.list = Collections.unmodifiableList(new ArrayList<>(incomingList));
return Collections.unmodifiableList(new ArrayList<>(internalList));
Java
  • equals/hashCode の定番
@Override public boolean equals(Object o) { /* フィールド比較 */ }
@Override public int hashCode() { /* フィールドから計算 */ }
Java

実務のメリットと注意点

  • スレッド安全: 共有しても状態が変わらないため、同期不要な場面が増える。
  • テスト容易: 副作用がなく、同じ入力に対し常に同じ出力。テストが安定する。
  • キャッシュしやすい: equals/hashCode と組み合わせて Map/Set に安全に格納可能。
  • 過剰なコピーに注意: 巨大な可変コレクションを毎回コピーするとコスト増。必要に応じて不変ビューや小さな差分設計を使う。
  • API設計の一貫性: 「変更は新インスタンス」ポリシーをドキュメントに明記して、誤用を防ぐ。

まとめ

  • フィールドは private final、setter なし、可変要素は防御的コピー。これで不変の骨格は完成。
  • 操作は「状態変更」ではなく「新しいインスタンスを返す」方針に統一。
  • record や java.time を活用すると、少ないコードで安全な不変オブジェクトが作れる。

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