LoginSignup
5
3

【Java】不変オブジェクトとスレッドセーフ・参照共有問題・シャロウコピーとディープコピー

Posted at

image.png

本記事ではセキュアなコーディングを行う上で必要な知識を、別名参照問題を入り口にしてまとめています。
言語には Java を用いていますが、Ruby・Python・PHP であればほとんど当てはまる様な内容かなぁ、と思います。

具体的には以下の事柄と関係するセキュアなコーディング方法を取り扱います。
普通のコーディングテクニックとして頻出するものばかりですが、落とし穴が多く存在します。何が危険なのか?なぜ危険なのか?どうやって解消するのか?など、具体的なコードも交えて説明したいと思います。

  1. 別名参照問題
  2. 不変(immutable)オブジェクト・可変オブジェクト
  3. 複製(シャロウコピー・ディープコピー)
  4. ディフェンシブコピー
  5. コピーコンストラクタ・static ファクトリ
  6. スレッドセーフ(ちょっとだけ)

主なソースは以下です。

また、ここでのメンタルモデルは猜疑心に基づく防御的プログラミングです。契約に基づくプログラミングや、信頼境界 (trust boundary)を超えた後でのプログラムでも全て適用すべきというわけではないと考えています。利用者が何らかの理由で信用できる、たとえば身内しかいないのであれば、ディフェンシブコピーは必要ない場合もあります。
防御的か契約か、については以下の過去記事をご覧ください。

信頼境界 (trust boundary)とは

  • プログラムに引かれた境界線
    • 一方では、データは信頼できない
    • 他方では、データは信頼できる(と想定)
  • 信頼できない側から入ってきたデータは、検証にパスしてはじめて、信頼できる側に移すことができる。

「この線が不明確(あるいは未定義)だと、脆弱性につながる」

引用元:4種類の信頼境界とセキュリティ構造 – 構造設計なしのセキュリティ対策?
image.png

目次

1. 別名参照問題は、参照と複製が明示的に記されないオブジェクト指向言語において発生する

前回、別名参照問題についての記事を書きました。

別名参照問題の本質は「共有されたオブジェクトが可変オブジェクトの場合、状態を意図せずに変更してしまう」ことです。
ここから見出せる解決策は2点です。

  1. 参照を共有しない
  2. 可変オブジェクトを共有しない

順番に説明していきます。まずは別名参照問題から。

下図のDate partyDate = retirementDate;のように代入時に同じインスタンスの参照値が代入されていることで起きる問題です。

引用元:AliasingBug
スクリーンショット 2022-12-25 18.51.54.png

これは、Java・Ruby・Python・PHP のような、参照と複製が明示的に記されないオブジェクト指向言語においてのみ発生し、プログラマが常に意識しなければいけない問題です。

変数への代入時、それが複製か共有か?を意識するのは、上記の様なOOPのみしか経験のない場合、中々難しいと思います。そもそもこの別名参照(エイリアス)問題の存在に気付くのも難しいのかもしれません。

C言語ではポインタという概念があり、参照か複製化を、プログラマが明示的に意識し記述する必要があります。
関数型言語では不変(immutability)が規則であり可変が例外であるという考え方があり、複数の変数が共有するインスタンスをどこかの変数で変更処理を行えばコンパイルエラーなどで防ぐことが出来ます。これらが言語レベルのサポートがなされていると、別名参照問題「共有されたオブジェクトの状態を意図せずに変更してしまう」
という場面は非常に置きづらいのだろうと思います。

これらを踏まえた上で考えると、参照と複製が明示的に記されないオブジェクト指向言語において発生する別名参照問題は以下の二点を意識すれば防ぐことが出来ます。

  1. 代入は複製のみに限定する
    1. 『参照を共有しない』
  2. オブジェクトや変数を不変( immutability )にする
    1. 『可変オブジェクトを共有しない』
      1. 他にも色々と方法はありますが根本的に無闇に可変オブジェクトを使用しなければ発生しません。
  • 複製とは基本的に「インスタンスそのものコピー(ディープコピー)」を指し、参照値のコピー(シャロウコピー)は指しません。
  • 不変オブジェクトとは「インスタンス生成後、そのインスタンスの状態(メモリ領域に保持されている値)が変化しない」という意味を指します。
  • 変数の不変性に関しては、ローカル変数に final 修飾子を付けることで不変となり再代入をコンパイルエラーで防ぐことが出来ます。

正確に言うと、クラスのメタデータを取得・操作するリフレクションという機能を使って変性は壊せます。また、不正目的の悪意あるユーザーから何も知らないがおかしなことをするユーザーまで様々なユーザが使用する中で、不変性は簡単に壊されてしまうかもしれません。それらに備えるための方法の一部も順番に紹介していきます。

補足ですが、不変性はマルチスレッドにおけるスレッドセーフであることや自由にキャッシュできるなどの利点がありますが、今の自分には自身の言葉で説明できないので、簡単に紹介しますが詳しくは割愛します。リフクレクションについても同様の理由で割愛です。

不変を破壊するリフレクション
import java.lang.reflect.Field;

public class RefrectionSample {
    private final int finalVar = 1;

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        RefrectionSample sample = new RefrectionSample();
        // クラスのメタ情報にアクセスして、フィールドのメタ情報も取得する
        Field field = RefrectionSample.class.getDeclaredField("finalVar");
        // メタ情報にアクセスしてアクセス可能性を不可から可能に書き換え、値をセットする
        field.setAccessible(true);
        field.setInt(sample, 100);
        System.out.println(sample.finalVar); // 1
        System.out.println(field.getInt(sample)); // 実質の値は100に変更されている
    }
}

1
100

リフレクションはDIに深く関わる技術です。DIは、ビジネスロジックがDBアクセス層に依存せずDBアクセス層がビジネスロジックに依存する依存性逆転を実現できるデザインパターンです。リフレクション・DIともに違う機会に掘り下げたいと思います。

2. 複製「シャロウコピーとディープコピー」

Java においてインスタンス変数は参照型です。オブジェクトをコピーをする場合、参照型変数とオブジェクト自身を区別して考える必要があります。プリミティブ型(intやdoubleなど)は、表す値を、何の追加メタデータもないビット・パターンとして直接的に符号化したものです。一方のオブジェクト参照は、Javaヒープ領域のアドレスを示すポインタです。Javaヒープとは、仮想マシンのみがガベージ・コレクションを通じて管理するメモリ領域です。

この辺りは過去記事「Java の評価戦略としての値渡しとメモリ管理の仕組み【Java には参照渡しはありません】」で解説してるので気になれば参照してください。

参照型変数の参照値(ポインタ)コピーか、参照しているインスタンスそのもののコピー(複製)が必要かを考慮しなくてはいけません。前者と後者のコピーは以下の様に呼ばれます。

  1. シャローコピー:shallow copy
  2. ディープコピー:deep copy

2-1. シャローコピー

シャロー(shallow )とは「浅い」という意味です。コピー元のオブジェクトから、同じ実体(インスタンス)を参照する別オブジェクトを生成する方式です。つまり、参照値のコピー・共有に他なりません。

上記で引用した画像でいえば以下の様なコードはシャロウコピーです

Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016"));
Date partyDate = retirementDate;

上記のコードでのメモリ管理と参照値の共有状態は以下になります。

image.png

この通り、二つのオブジェクトの指し示すインスタンスは同一なので別名参照問題を引き起こします。この参照型の特徴に注意してコーディングしないと、クラス定義などによるデータのカプセル化やプログラムのモジュール化が台無しになってしまう危険があります。Java におけるcloneメソッドによるコピーは、オーバライドでロジックが変更されない限りシャロウコピーです。

プリミティブ型においては、プリミティブ型の挙動によってあらたなヒープ領域を確保しそこに値がコピーされますが参照型ではシャロウコピーになります。ただし、Stirng型においては異なります。実態はchar型の配列であり、さらに不変型です。その他ラッパークラスもイミュータブルな参照型な為、代入によってインスタンス変数を作成するたびにヒープ領域が新たに確保され、あたかもプリミティブ型の変数かのような挙動となります。

2-1-1. クローンメソッドについて

cloneメソッドはコンストラクタを呼ばずにオブジェクトを生成する手段です。オブジェクトのコピーを返すメソッドですが、Javaのcloneメソッドは特殊です。Java以外の言語(PHPやRubyなど)のcloneメソッドではディープコピーを提供しますが、Javaのcloneメソッドでは、シャローコピーが提供されています。

不変オブジェクト(状態が変更されないオブジェクト)のみシャローコピーで問題ありません。不変オブジェクトは内部の状態が変わることが無いため、ディープコピーを行う必要が無いからです。参照のコピーで構いません。

可変オブジェクト(状態が変更されるオブジェクト)をディープコピーとして提供出来るようにするにはcloneメソッドをオーバライドしロジックを変更するか、下記で紹介する原始的なディープコピーを行う必要があります。

ただし、Effective Java には「cloneを注意してオーバーライドする」というセクションがあります。ここでは7ページにも渡り、注意点が説明されていてその中で欠陥があるやら問題があるやらと明言されてます。最終的には以下の様に代替案を提案しています。

本当にこれだけの複雑さが必要なのでしょうか。それはまれです。もし、 Cloneable を実装しているクラスを拡張するのであれば、正しく振る舞う clone メソッドを実装する以外の選択肢はほとんどありません。さもなければ、オブジェクトのコピーを行う何らかの代替手段を提供するか、 オブジェク トの複製を単に提供しない方がおそらく賢明です。 たとえば、不変クラスがオブジェクトのコピーをサポートすることはほとんど意味がありません。 なぜならば、 コピーされたものは、元のオブジェクトと実質的に区別がつきません。
オブジェクトのコピーに対する上手い方法は、 コピーコンストラクタ (copy constructor) かコピーファクトリー (copy factory) を提供することです。

コピーコンストラクタ (copy constructor) 、コピーファクトリー (copy factory) については別項で解説します。

2-2. ディープコピー

シャロウコピーとは違い、ヒープ領域に新たに領域を確保し、その領域に格納する値をコピーした後、新たな領域を指し示すアドレス値を変数に格納するような形でコピーするのがディープコピーです。上記でのプリミティブ型の挙動はディープコピーそのものです。この辺りも過去記事「Java の評価戦略としての値渡しとメモリ管理の仕組み【Java には参照渡しはありません】」で具体的に解説してます

Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016"));
Date partyDate = new Date(retirementDate.getTime());

image.png

上記での方法はかなり原子的な方法です。この様にデータの状態が新たなヒープ領域に格納された状態を「ディープコピー」と言います。

代入とはこの様に常にディープコピーであるべきであり、複製とはディープコピーの文脈として用いるこ^のが適当な単語だと思います。これで別名参照問題はある程度は回避できます。

Javaではポインタか参照かを明示的に記述する必要がない、という言語的な特徴によってこの辺りがややこしくなってしまったのですね。ポインタという概念は、初学者時代の自分には鬼門だったと記憶しています。今となっては避けては通れないと感じています。

代入作業の度に複製なのか参照なのか?を気にするのははっきり言って無駄でしかないと感じています。再代入が好ましくないのは当然として、代入時は複製を徹底することが基本でいいのではないでしょうか。参照を用いるべき場面としては、インスタンス生成コストがパフォーマンスに影響を及ぼす処理の場合のようです。

引用元:コードコンプリート第二版 上 6.3.4 コンストラクタ

■ シャローコピーを使用する理由が特になければ、ディープコピーを優先する
 複合オブジェクトで最も悩むのは、オブジェクトのディープコピー(深いコピー)を実装するのか、それともシャローコピー(浅いコピー)を実装するのかである。「深い」と「浅い」の意味は状況によりけりだが、オブジェクトのディープコピーとは、オブジェクトのメンバデータをメンバごとにコピーするものだ。これに対し、シャローコピーは1つのオブジェクトをポイントするか、または参照するだけの参照コピーである。

 シャローコピーを作成する目的は、一般に、パフォーマンスを向上させることである。確かに、大きなオブジェクトのコピーをいくつも作成するのは、見た目が良くないかもしれないが、パフォーマンスに無視できないほどの影響を及ぼすことはめったにない。一方、パフォーマンスを低下させているのが少数のオブジェクトの場合、プログラマは周知のとおり、原因となっているコードを突き止めるのがへたである。パフォーマンスが改善するという確証がないのに複雑さを増大させることは、良い打開策であるとは言えない。ディープコピーとシャローコピーを選択する良い方法は、シャローコピーを使用すべき根拠が明確になるまでは、ディープコピーを使用することだ。

 ディープコピーはシャローコピーよりもコーディングや保守が容易である。シャローコピーは、どちらの方法でコピーされたオブジェクトにも含まれているコードの他に、参照をカウントするコード、オブジェクトを確実にコピー、比較、削除するためのコードなどを追加する。このようなコードはエラーの原因になりやすいので、シャローコピーを作成する理由が特になければ、避けた方がよいだろう。

3. 不変性(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);
    }
}

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

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

ディフェンシブコピーなどについては後述します。

3-3. 不変オブジェクトの利点

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

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

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

3-4. 不変オブジェクトの欠点

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

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

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

3-5. スレッドセーフ

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

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

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

image.png

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

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

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

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

3-6. ディフェンシブコピー「不変オブジェクトの落とし穴」

不変オブジェクトが満たすべき5つの要件
で説明した以下の部分で、ディフェンシブコピーが登場しました。
不変オブジェクトが参照型な可変オブジェクトを、「参照する・渡される・戻り値として返却する」場合は注意が必要です。
可変オブジェクトはその性質上、参照するたびその値が変わる可能性を常に考慮しなくてはいけません。つまり、参照するたびその値が異なっていることを想定しなくてはいけません。

  1. クラスが保持するフィールドに可変オブジェクトがあった場合、それに対する独占的アクセスを保証する
    1. クラスが可変オブジェクトを参照している場合、呼び出し側がそのオブジェクトへ参照できないことを保証する
    2. つまり、その可変オブジェクトにアクセス可能なのは不変クラスである自分自身のみの状態にする
    3. 保持フィールドに限らず、呼び出し側から引き数で渡された可変オブジェクト参照を用いた初期化にも注意が必要
      1. 参照型はメモリ上に展開されたインスタンスの参照値を格納している。受け取った不変オブジェクト内で不変性を保っても、呼び出し側でそれを容易に崩せる(別名参照問題)
        1. 後々呼び出し側で参照先の内容を変更することができてしまう
      2. 呼び出し側から渡された参照型や可変オブジェクトを使用する場合は、ディフェンシブコピーを利用する

メソッド呼び出し・インスタンス生成時コンストラクタ実行時、呼び出し側が参照型を引数で渡す場合や呼び出された側が参照型のオブジェクトを戻り値として返してしまい、かつその参照しているオブジェクトが可変の場合は困ったことが起きます。

  • 呼び出し側と呼び出された側で、参照の共有をしてしまう

これは、そのまま別名参照問題です。呼び出された側でどんなに不変性を保とうとしても不変性が簡単に崩れてしまいます。これは上記の言葉で言えば、「クラスが保持する参照先可変オブジェクトに対する独占的アクセスを保証できていない」状態です。もし共有している参照先のオブジェクトが不変であればこの共有は気にする必要はありません。共有していようが状態の変更が出来ないのであれば、ディフェンシブコピーの必要は無くなります。

それでは可変なオブジェクトを呼び出し側と呼び出せれた側で共有している例を以下に記します。
以下の Period クラスは二つの Date インスタンスを受け取り、「終わりは開始よりも後である」という条件を満たし、かつそれが不変であることを表現しようとしています。前提条件として、コンストラクタで start < end の条件が成立するようにチェックを行っています。

引用元:Effective Java

不完全な不変クラス
public class Period {
    private final Date start;
    private final Date end;

    /**
     * @param start 期間の開始
     * @param end 期間の終わり。開始より前であってはならない。
     * @throws IllegalArgumentException start が end の後の場合。
     * @throws NullPointerException start か end が null の場合
     */
    public Period(Date start, Date end) {
        // start < endの不変式の前提条件チェック
        // 実際の計算や処理を行う前にパラメータをチェックする
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }

.............省略
}

しかしこれでは不変性を実現することは出来ません。理由を列挙します。

  1. Date オブジェクトには、setYear() / setTime() メソッドが存在するため可変オブジェクト
    1. 可変オブジェクトの参照を外部から取得できるようにすると、その参照を通して値を変更されてしまいます。
  2. インスタンスフィールドが private で final だが、可変オブジェクトの参照をそのまま用いてインスタンス生成している
    1. 参照が呼び出し側と共有されているので、インスタンス生成後に状態を変更できる
    2. インスタンスフィールドが参照型なので、 final で修飾しても参照先の状態変更を禁止しているわけではない
  3. getter でも可変インスタンスフィールドの参照をそのまま返しているので、変更を加えれば不変を崩せる
    1. カプセル化を崩し、内部状態を漏らしています。可変オブジェクトへの参照を返してはいけません。
  4. 値代入の前にパラメータの正当性検査。これは検査後と値の代入間の隙間で攻撃を許してしまいます。
    1. Time of Check/Time of Use 攻撃。コピー作成後に正当性検査など全ての処理を行う必要があります。

公開されたAPIや、悪意ある呼び出し側から呼び出される可能性のあるプログラムに、上記の様な不完全な不変オブジェクトを提供している場合、攻撃の起点となってしまいます。
以下の様なコードを見てみましょう

偶然あるいは悪意を持ってクラスの内部状態を変更する
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);

// 呼び出し側が不変性を崩す
end.setYear(1000);

// end への参照を getter で取得し不変式(start < end)を崩す
Date e = period.getEnd();
e.setTime(e.getTime() - 1000);

この様に不完全な不変オブジェクトは、呼び出し側で簡単に不変性を崩すことができます。
いずれも呼び出し側の startend、呼び出された側のPeriodオブジェクト間で可変オブジェクトへの参照値を共有してしまっているのが問題です。これを防ぐためには、「そもそも可変オブジェクトを扱わない」か「ディープコピーを返す」かのどちらかです。前者であればjava.time.LocalDateTimeなどを使用すれば良さそうです。

引用元:クラスLocalDateTime

実装要件:このクラスは不変でスレッドセーフです。

後者をディフェンシブコピーと呼びます。

不変クラス
public class Period {
    private final Date start;
    private final Date end;

    /**
     * @param start 期間の開始
     * @param end 期間の終わり。開始より前であってはならない。
     * @throws IllegalArgumentException start が end の後の場合。
     * @throws NullPointerException start か end が null の場合
     */
    // コピーを返すコピーコンストラクタ
    public Period(Date start, Date end) {
        // ディフェンシブコピーを先に行う
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        // コピー後にコピーした引数に対し正当性チェックを実施する(TOCTOU脆弱性対策)
        if(this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException(this.start + " after " + this.end);
        }
    }

    public Date getStart() {
        // ディフェンシブコピー
        return new Date(start.getTime());
    }

    public Date getEnd() {
        // ディフェンシブコピー
        return new Date(end.getTime());
    }

.............省略
}

クラスの可変な内部状態をディープコピーしその参照を返すようにすると、コピーされるオブジェクト(引数で渡された参照先のオブジェクト)の変更可能であることに変わりはないですが、呼出し側はディープコピーされたオブジェクトの内部状態にはアクセスできません。これにより参照値の共有を回避できます。これがディフェンィブコピーです。

信頼できないコードが、可変な引数を使ってアクセッサメソッドを呼び出すことが可能な場合、引数がインスタンスフィールドに保存される前にそのディフェンシブコピーを作成します。

注意点は以下になります。

  1. パラメータの正当性検査はディフェンシブコピーのあとに行う
  2. java.util.Dateのような可変クラスの場合、コピーにclone()を使わない
  3. コンストラクタ内のみならずアクセサメソッドでもディフェンシブコピーを行う
  4. クラスのインスタンスフィールドが final 宣言されている場合のみに有効

3-6-1. パラメータの正当性検査は防御的コピーのあとに行う

マルチスレッドで Period の生成を行っている様な状況を想定します。
ディフェンシブコピーの前にパラメータの正当性検査をした場合、検査後とコピーされて値が代入されるまでの間が「無防備な時間」として存在してしまいます。この無防備な時間に、別のスレッドが参照先のオブジェクトの状態を変更できる時間が生じるためです。これを防ぐために、先にディフェンシブコピーを行います。

3-6-2. java.util.Date のような可変クラスの場合、コピーに clone メソッドを使わない。

Date クラスは final つまり不変クラスではありません。clone メソッドを無条件で使用できるのは不変クラスに対してのみです。可変クラスに対しても不変クラスに対しても、Java の clone メソッドはシャロウコピーしか行いません。不変オブジェクト(String など)に関してはシャロウコピーで不変オブジェクトの参照を共有しても問題ありませんが、可変オブジェクトには問題があります。

なので、詳細は割愛しますが、clone メソッドを使用する場合は様々な条件を満たす必要があり、clone メソッドでディープコピーできる様にオーバーライドする必要があります。これを悪用し、Date クラスなどの可変オブジェクトのサブクラスを渡し、そのサブクラス内でディープコピーを返さずになんらかの攻撃を行う様オーバーライドされた clone メソッドを作成することもできます。

不変クラスであれば拡張不可なためサブクラスは作成されません。攻撃の観点から見ても、不変オブジェクトであれば clone メソッドでコピーを作成しても問題ありません。信頼できないものがサブクラス化できる型のパラメータをディフェンシブコピーするために、clone メソッドを使用するのは厳禁です。

3-6-3. コンストラクタ内のみならずアクセサメソッドでもディフェンシブコピーを行う

getter で不変を崩す
// end への参照を getter で取得し不変式(start < end)を崩す
Date e = period.getEnd();
e.setTime(e.getTime() - 1000);

この様な変更を防ぐためにも、getter 内でディフェンシブコピーを行います。 Period クラスこれにより、本当の意味での不変クラスとなります。信頼できないコードに可変な内部状態を返す場合、そのディフェンシブコピーを作成して返さなくてはいけません。

3-6-4. クラスのインスタンスフィールドが final 宣言されている場合のみに有効

コピーコンストラクタというアプローチは、クラスのインスタンスフィールドが final 宣言されている場合にのみ有効です。呼出し元は、既存の DateClass のインスタンスを引数としてコピーコンストラクタを呼び出すことで、コピーをリクエストします。

3-7. 可変オブジェクトの状態変更を防ぐ方法

ディフェンシブコピーを行う必要があるのは可変オブジェクトの参照値を使用する場合でした。不変オブジェクトの欠点であるオブジェクトの生成コストですが、それはディフェンシブコピーでも当てはまります。もし、この生成コストが許容されない場合はどの様にすればいいのでしょうか?そういった場合は、Javadocにその旨の記載をするといった対処が最低限必要ですが、一つの方法として以下を紹介します。

その様な場合の代替案として、変更不可能なビューを作成するという方法があります。
引用元:適合コード (変更不可能な Date のラッパー)

可変オブジェクトのコピーの作成が不可能であるもしくはコストがかかる場合、変更不可能なビュークラスを作成するという方法もある。このクラスは例外をスローするために可変メソッドをオーバーライドし、可変クラスを保護する。

可変オブジェクトのディフェンシブコピーを作成するのが不可能、もしくはコピーにコストが掛かるような場合は以下の方法で代替が可能です。可変オブジェクトの変更不可能なビューを作成しそちらを参照させる方法です。変更不可能なビューは 可変オブジェクトのクラスを継承するか、可変オブジェクトの型を定義するインタフェースを実装し、オブジェクトの状態を変更するメソッドを全てオーバーライドし例外として UnsupportedOperationException を投げるようにし、可変クラスを保護します。

class UnmodifiableDateView extends Date {
    
    private Date date;
    
    public UnmodifiableDateView(Date date) {
        this.date = date;
    }

    // 状態変更を行うメソッドは例外を投げる。
    @Override
    public void setTime(long time) {
        throw new UnsupportedOperationException();
    }

}

ublic class Period {
    private final Date start;
    private final Date end;
~~~~~~~~~ 中略 ~~~~~~~~~~
    public Date getStart() {
        return new UnmodifiableDateView(start);
    }

    public Date getEnd() {
        return new UnmodifiableDateView(end);
    }


}

これで可変オブジェクトに存在する setter を介した状態の変更を禁止できます。  
可変オブジェクトのクラスが継承不可能で、型を定義するインタフェースもない場合には この方法は適用できません。

4. コピーコンストラクタ・public な static ファクトリメソッド + private なコンストラクタ

信頼できないコードにインスタンスを安全に渡すため、可変クラスにはコピー機能を実装する必要があります。

可変オブジェクトを別名で参照共有した場合、外部からの意図しないあるいは悪意ある変更を受け入れてしまうため、可変クラスはコピーコンストラクタか、インスタンスのディープコピーを返す public static なファクトリメソッドのいずれかを実装しなくてはいけません。あるいは、java.lang.Object の clone メソッドをオーバーライドし、ディープコピー機能を実装するかです。ただし、上記で説明した通り clone メソッドを安全に使用できるのは、不変クラスに対してのみであり、不変でない・信用できなクラスに対してこれを行ってはいけません。

信頼できる呼出された側は、オブジェクトのインスタンスを信頼できないコードへ渡す前に、提供されたコピー機能を使ってディフェンシブコピーを作成することができます。信頼できない呼出された側はそのようにディフェンシブコピーを作成できません。なので、コピー機能を用意したとしても、信頼できないコードから受け取った入力や、信頼できないコードに返される出力のディフェンシブコピーを作成する必要がなくなるわけではありません。悪意ある攻撃者と参照値を共有するのはどんな場合でも防がなくてはいけないからです。信頼できないコードには破棄されても構わないコピーを渡せるよう、可変クラスにはコピーを作成する手段を実装する必要があります。

それが『コピーコンストラクタ』と『public な static ファクトリメソッド + private なコンストラクタ』
の二つです。このアプローチはインスタンスフィールドが final である場合のみ有効です。ちなみに clone メソッドの使用でも実現する方法はありますが、考慮事項が多く複雑かつ clone メソッドでなくてはいけないパターンを想定できないため割愛します。

コピーコンストラクタはすでに上記の参考コードで登場しております(コメントあります)。
コピーコンストラクタは、既存のオブジェクトのコピーとして新しいオブジェクトを作成するためのコンストラクタで、ディープコピーを返すことでディフェンシブコピーを実行していました。

4-1. 『public な static ファクトリメソッド + private なコンストラクタ』

public static ファクトリメソッドとコンストラクタの private 化は不変性を保証する手段の一つでもあります。
静的なコピーファクトリーメソッドを使用して、本質的にコピーコンストラクタメソッドと同じことを行うことができます。

不変性を保証するため、不変クラスではサブクラス化を許さないことが必要です。方法としてクラス自体を不変にする事が基本的な方法です。ですが、コンストラクタを private、またはパッケージプライベートにし、代わりに public な static ファクトリメソッドを提供するこちらの方法がより柔軟です。スーパークラスによっては、信頼できるサブクラスによる拡張は許可しつつ、信頼できないコードによる拡張は阻止しなくてはならないものもあります。そのようなスーパークラスを final 宣言すると、信頼できるサブクラスからの拡張を妨げることになるため、柔軟性に欠けます。よって、クラスの継承は慎重に設計する必要があります。

コピーコンストラクタではコンストラクタは public ですが、ここでのコンストラクトは private となり、可視性は自分自身に限られ、当該クラスのインスタンス・サブクラスの生成不可を呼び出し側に強制します。不変性の一つの条件を担保することになります。

継承を制限することで発生する不利益は、実際には問題ありません。そもそも継承ではなく、コンポジションの使用で十分な場合がほとんどだからです。また、継承の使用にはリスクとコストが発生するので、使用には慎重になる必要があります。

static ファクトリメソッド自体はオブジェクトの生成を制御する手法としての役割もあり、どちらかといえばこちらの役割の方がメジャーな様に感じます。ただ、オブジェクトの生成を制御したいという目的のため、既存のコンストラクタを public にしていてはあまり意味がない様にも思えるのでやはりその意図であっても private なコンストラクタにするべきではないのかな〜と思います。まあ、ぶっちゃけ場合によりけりとしかいえないですが。

static ファクトリメソッド
public class Period {
    private final Date start;
    private final Date end;

    // インスタンス・サブクラスの作成はできない
    private Period(Date start, Date end) { 
        // ディフェンシブコピー
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if(this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException(this.start + " after " + this.end);
        }
    }

    public Date getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }

    public static PeriodClass newInstance(Date periodStart, Date periodEnd)  {
        return new PeriodClass(periodStart, periodEnd);
    }
.............省略
}

5. 終わりに

正直、考慮不足な点やまだまだ勉強不足な点もあり網羅的な内容にできなかったなーと思います。現時点での自分の理解度はここまでです。より理解度を深めれたらまた記事にしたいと思います。

あと、正直これまでの内容は理想であり現実的に常に厳守できるのかどうかは別かと思います。大事なのは考慮すべきことを考慮した上でやるかやらないかを検討しどちらかを選択した際のメリットデメリットやトレードオフを知った上で、何をするかしないかを決定することだと思います。

以下に過去記事を紹介しています。よかったらご覧ください。最後までお読みいただき有難うございました!!

5
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3