不変オブジェクトの作り方(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;
}
public 型 a() { return a; }
public 型 b() { 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 を活用すると、少ないコードで安全な不変オブジェクトが作れる。
