よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

不変(Immutable)オブジェクトについて

こちらの記事は以下の記事にて他のトピックと共に統合的に扱っています。ちょっと長いですが、多角的により深く、ある程度網羅的にまとめています。こちらの方がより深い理解が得られると思いますので時間がある場合はこちらを参照してください。

yoshitaro-yoyo.hatenablog.com

不変性(immutable)

Immutable(不変)とは、初期化・生成された後に値の変更ができないオブジェクトや変数の性質を指します。

インスタンス化された後、状態が変わらないオブジェクトのことを不変オブジェクト、インスタンス化すると不変オブジェクトになるクラスを 不変クラスと呼びます。

文字列(String)などのラッパークラスもこの性質を持ちます。ラッパークラスの代表的な存在意義としてカプセル化における値と振る舞い(メソッド)の一つのまとまり、がまず出てきますが不変性も非常に重要な性質です。下記の参考のコードではプリミティブ型のフィールドを final で定義してますがそれと同様にラッパークラスが持つ値は final で定義されています。

public class ImmutableValue {
    private final int value;
    public ImmutableValue(int value) { 
        this.value = value; 
    }
    // 値を変更したいときは、新しいオブジェクトを生成して返す
    public Immutable modify(int value) 
    { 
        return new ImmutableValue(value);
    }
}

不変オブジェクトが満たすべき5つの要件

  1. オブジェクトの状態を変更するためのメソッドを何一つ提供しない
    1. setter メソッドを提供しない。setter は悪。マジ☆悪☆即☆斬☆
    2. getter メソッドを提供する場合は、可変インスタンスフィールドの参照を返してはいけない
  2. クラスが拡張(extends)できないことを保証する
    1. クラスに final 修飾子をつける。継承を禁止できます。
    2. より柔軟な方法として、コンストラクタを private にして、public で static なファクトリメソッドを提供する
    3. もし、不変にできないやむを得ない理由があるならば、可変性を制限する
  3. すべてのフィールドを final にする
    1. クラスフィールドやインスタンスフィールドが参照型の場合、finalを付けても参照先のオブジェクトの状態の変更を禁止することはできない。
      1. 禁止できるのは参照値の変更のみ、つまり参照するインスタンスの変更ができないだけで状態は変更できる。final だからと public にすると容易に外部から誰でも状態を変更できるので危険。全てのフィールドは private にすべき
    2. 例外:内部でのみ利用する計算結果をフィールドでキャッシュしておきたい場合はその限りではない
  4. すべてのフィールドを private にする
    1. 可変なオブジェクトを参照するインスタンスフィールドがあったとしても、呼び出し側から見えない様にしておけば、参照を辿ってオブジェクトを変更することは不可能になります。
  5. クラスが保持するフィールドに可変オブジェクトがあった場合、それに対する独占的アクセスを保証する
    1. クラスが可変オブジェクトを参照している場合、呼び出し側がそのオブジェクトへ参照できないことを保証する
    2. つまり、その可変オブジェクトにアクセス可能なのは不変クラスである自分自身のみの状態にする
    3. 保持フィールドに限らず、呼び出し側から引き数で渡された可変オブジェクト参照を用いた初期化にも注意が必要
      1. 参照型はメモリ上に展開されたインスタンスの参照値を格納している。受け取った不変オブジェクト内で不変性を保っても、呼び出し側でそれを容易に崩せる(別名参照問題)
        1. 後々呼び出し側で参照先の内容を変更することができてしまう
      2. 呼び出し側から渡された参照型や可変オブジェクトを使用する場合は、ディフェンシブコピーを利用する

不変オブジェクトの利点

  1. オブジェクト内部の状態遷移を意識する必要がないためシンプル
  2. 状態が変化することがないため、自由にキャッシュできる
  3. スレッドセーフで安全に共有できる
  4. 共有しても状態が変わらないので、ディフェンシブコピー・コピーコンストラクタ・cloneメソッドの実装が不要
    1. Javaの clone メソッドはシャロウコピーです。ところが、不変オブジェクトの場合はシャロウコピーでもオーバーライドが不要です。値が変わらないのであればディープコピーで新たなメモリ領域を確保してインスタンスを生成する必要がありません。

他にもいくつかありますが、自身の言葉では説明できないので、興味ある方は調べてみてください。

  • 参考
    • エラーアトミック性
    • キャッシュの使い所
    • ハッシュバケットに基づいたコレクションのキーに適している

不変オブジェクトの欠点

不変クラスではオブジェクトに対して操作や演算を行いたい場合は、操作や演算を行った結果を表す別のオブジェクトを生成して返します。複数の計算ステップそれぞれで、その場限りのインスタンスを生成しては破棄する、といった場合です。そのため、オブジェクトの生成にコストがかかるような場合に パフォーマンスに影響が出てきます。特に 大きなオブジェクトのうちのほんの一部だけ変更したオブジェクトを生成するような場合は可変クラスに比べて性能が大きく劣ります。

解決方法としてパッケージプライベートの可変のコンパニオンクラスを作成し、内部的にはそのコンパニオンクラスを利用するという方法があります。

  • 不変クラスの String に対する StringBuilder のような、public で可変コンパニオンクラス

スレッドセーフと不変オブジェクト

不変なオブジェクトは、状態を変えることはできないので、いかなるコードがアクセスしていても状態が同じであることが保証されます。

マルチスレッドの様な多くの人が一度に同時アクセスする環境で、ある一つの可変なインスタンス(メモリ上に展開されたオブジェクトのデータ)を複数のスレッド・複数のユーザで共有されている場合、このうちの誰かがインスタンスのもつ状態、つまりデータの値を変更してしまうと変更していないユーザにも影響が及びます。別名参照問題と同じですね。

可変インスタンスを共有すると、いつどこでどの様に変更されたかを把握する必要が出てきますし、不正な行いも可能になってしまいます。Calendarクラスというのは可変オブジェクトの代表例ですが、好きなタイミングで変更が可能です。Aさんから見た時、Calendar オブジェクトが指し示す時間は今日だったとしても別の誰か、Bさんに同時に共有・参照されていて、Bさんが別のスレッドで Calendar オブジェクトの時間を一ヶ月前に書き換えたとします。

image.png

Aさんからすると、書き替えられていることを知らずに今日だと思って同時に参照された Calendar オブジェクトを使用してしまいます。使用されているインスタンスの状態が、いつどこで誰が使用しても「変わらない」ことが保証されていない状態は危険です。変わっていないことを保証するのが不変オブジェクトです。

ですが不変オブジェクトだからスレッドセーフなんだ!と簡単に思ってはいけないそうです。 スレッドセーフにおける不変オブジェクトの働きを考慮する上での注意点としては以下、詳しくはリンク先をご覧ください。ぶっちゃけ現時点の自分では理解できんです。

引用元:VNA01-J. 不変オブジェクトへの共有参照の可視性を確保する

不変(immutable)オブジェクトへの共有参照は、参照が更新されるとただちに複数スレッド間で可視となる、と誤解されることが多い。たとえば、不変オブジェクトだけ参照するフィールドを含むクラスは、クラス自身が不変であり、それゆえスレッドセーフである、という誤った認識を持つプログラマは少なくない。

その他、不変オブジェクトには落とし穴が幾つかあります。深く関連するのが別面参照問題やディフェンシブコピーです。別記事にまとめたいと思います。

あと、スレッドセーフを運用するための入口には来れたのかなと思いますが、奥が見えなすぎですね。
並列処理における要件の安全性・活性・処理性能、可視性やアトミック性、ロックを使った同期(排他制御)、synchronizedによる排他制御、など理解しておくべき事柄が多すぎて今のところしばらく手が出せなそうです。おいおいやっていきます。