直ちにItem 50防御コピーを作成する

12003 ワード

Javaは安全な言語です.これがjavaを書く楽しみの一つです.
ネイティブメソッドを使用しないで、C、C++などの不安全な言語でよく見られるバッファオーバーフロー、配列オーバーフロー、ワイルドカード
メモリクラッシュエラーでは安全です.Javaで作成されたクラスは、システムの他の部分で何をしても変わらない.
これは、メモリ全体を巨大なアレイとして使用する言語では享受できない利点です.
しかし、いくら期待しても、他の階層からの侵害を阻止しようと努力しないわけにはいかない.
しかし
クライアントが皆さんの不変の観念を破るために頭を絞ってプログラミングしていると仮定します.
任意のオブジェクトがそのオブジェクトの許可を得ずに、外部で内部を変更することはできません.
しかし注意しないと、思わず内部修正を許してしまう.
持続時間(period)を表す次のクラスは、いったん値が確定したら変更しないつもりです.
コード50−1の期間を表すクラス−不変式を遵守できない.
public final class Period {
    private final Date start;
    private final Date end;
    
    /**
     * @param start 시작 시각
     * @param end 종료 시각; 시작 시각보다 뒤여야 한다. 
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다. 
     * @throws NullPointerException start나 end가 null이면 발생한다. 
    */
    public Period(Date start, Date end) {
        if(start.compareTo(end) > 0)
            throw new IllegalArgumentException(
                start + "가 " + end + "보다 늦다.");
        this.start = start;
        this.end = end;
    }
    
    public Date start() {
        return start;
    }
    
    public Date end() {
        return end;
    }
    
    ... // 나머지 코드 생략
}
このレベルは変わらないように見えますが、開始時間が終了時間より遅れてはいけないという理由はありません.
しかし,Dateが可変であるという事実を利用すれば,その不変性を簡単に打ち破ることができる.
攻撃コード50-2 Periodインスタンスの内部.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정했다!
幸いなことに、Java 8以降は簡単に解決できます
Dateの代わりに不変のInstantを使えばいいです.
(LocalDateTimeまたはZonedDataTimeを使用することもできます).
Dateは古いAPIで、新しいコードを書くときに使用できません.
しかし、使わなくても問題は解決できません.
Dateのように可変な古い値タイプを使用する期間が長すぎるため,依然として多くのAPIと内部実装の残りがある.
本プロジェクトは、以前に作成された古いコードに対応することを目的としています.
Periodインスタンスを外部から保護するには
ジェネレータから受信した各可変パラメータを防御的にコピーする必要があります.
次に、Periodインスタンスで非汎用コピーを使用します.
コード50-3修正ジェネレータ-パラメータの防御コピーを作成
public 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 enw IllegalArgumentException(
            this.start + "가" + this.end + "보다 늦다.");
}
新しく作成したジェネレータを使用すると、前の攻撃はPeriodに脅威を与えなくなります.

パラメータを検証する前に、防御コピーが作成され、検証に使用されていることに注意してください。


順番が不自然に見えますが、このように記入しなければなりません.
マルチスレッド環境の場合、元のオブジェクトを検証してコピーを作成した瞬間、他のスレッドは
元のオブジェクトを変更するリスクを無視します.
パラメータの検証前に防御レプリケーションを実行すると、このリスクを回避できます.
コンピュータセキュリティコミュニティでは、チェックポイント/使用ポイント(time-of-check/time-of-use)攻撃や英語タグを減らすことで、TOCTOU攻撃と呼ばれています.
パラメータがサードパーティによって拡張可能なタイプの場合は、防御コピーを作成するときにcloneを使用しないでください.
コード50-4サイクルインスタンスに対する2回目の攻撃
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // p의 내부를 변경했다!
この攻撃を防ぐには,アクセス者が可変フィールドの防御コピーを返すだけでよい.
コード50-5によって変更されたアクセス者-フィールドの防御コピーを返します.
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}
新しい訪問者がいれば、Periodは完璧な不変になります.

コアの整理

  • クラスがクライアントから受信またはクライアントに戻るコンポーネントが可変である場合、防御的なレプリケーションが必要です.
  • レプリケーションのコストが高すぎるか、クライアントがこれらの要素を誤って変更していないと信じている場合は、ドキュメントでこれらのコンポーネントを変更する責任は、防御的なレプリケーションではなくクライアントにあることを説明します.