Effective Java-シーケンス化

7371 ワード

ディレクトリ七十四、Serializableインタフェース七十五を慎重に実現し、カスタムシーケンス化形式七十六、保護性を考慮したreadObjectの作成方法
七十四、Serializableインタフェースを慎重に実現
        1つのオブジェクトを1つのバイトストリームに符号化することをオブジェクトシーケンス化と呼び、反対の処理、すなわちバイトストリーム符号化からオブジェクトを再構築することを逆シーケンス化と呼ぶ.Serializableインタフェースを実装する最大の代価は、クラスがパブリッシュされると、「このクラスの実装を変更する」柔軟性が大幅に低下することです.2つ目の代価は、バグやセキュリティ・ホールが発生する可能性を高めることです.3つ目の代価は、クラスが新しいバージョンを発行するにつれて、関連するテストの負担も増加したことです.
        継承のために設計されたクラスでは,Serializableインタフェースをできるだけ少なく実現すべきであり,ユーザのインタフェースもできるだけ少なくSerializableインタフェースを継承すべきである.内部クラスもSerializableを実装すべきではありません.
七十五、カスタムシーケンス化形式の使用を考慮する
        クラスのシーケンス化形式を設計することは、クラスのAPIを設計することと同様に重要であるため、デフォルトのシーケンス化形式が適切かどうかを真剣に考慮しない前に、デフォルトのシーケンス化行為を軽率に使用しないでください.決定する前に、柔軟性、性能、正確性の複数の角度からこの符号化形式を考察する必要があります.一般的に、デフォルトのシーケンス化形式は、独自に設計されたカスタムシーケンス化形式がデフォルトの形式とほぼ同じである場合にのみ受け入れられます.たとえば、オブジェクトの物理的表現が論理的な内容と同等である場合、デフォルトのシーケンス化形式を使用するのに適している可能性があります.次のコード例を参照してください.
public class Name implements Serializable {
    private final String lastName;
    private final String firstName;
    private final String middleName;
    ... ..
}

        論理的観点から、クラスの3つのドメインフィールドは、その論理内容を正確に反映する.しかし、デフォルトのシーケンス化形式が適切であっても、前の例のコードではfirstNameとlastNameがnullではないなど、制約関係とセキュリティを保証するreadObjectメソッドを提供する必要があります.
        極端な例を見てみましょう
public final class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;
    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
}

        上記のサンプルコードでは、デフォルトのシーケンス化を使用すると、双方向チェーンテーブルの各ノードのデータおよび前後関係がシーケンス化されます.したがって、この物理表現が論理データの内容と実質的に異なる場合、デフォルトのシーケンス化形式を使用すると、以下の欠点があります.        1.このクラスの導出APIをそのクラスの内部表現法に永遠に束縛し、今後より良い実現方式を見つけても、従来の実現方式から抜け出すことができない.        2.スペースを消費しすぎます.実際には、上記のサンプルコードでは、データ部分をシーケンス化するだけで、チェーンテーブルノード間の関係を完全に無視することができます.        3.時間がかかりすぎます.        4.スタックオーバーフローが発生します.
        以上の4点から、StringListクラスのシーケンス化の実装方法を改訂しました.以下のコードを参照してください.
public final class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;
    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next)
            s.writeObject(e.data);
    }
    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int numElemnet = s.readInt();
        for (int i = 0; i < numElements; i++)
            add((String)s.readObject());
    }
    public final void add(String s) { ... }
    ... ...
}

        なお、StringListのすべてのドメインは瞬時であるにもかかわらず、writeObjectメソッドの主なタスクはdefaultWriteObjectを呼び出すことであり、readObjectメソッドの主なタスクはdefaultReadObjectを呼び出すことである.すべてのインスタンスドメインが瞬時である場合、技術的にはdefaultWriteObjectとdefaultReadObjectを呼び出さないことも可能ですが、推奨されません.今後の変更では、クラスに非transientドメインフィールドを追加する可能性が高いため、writeObjectまたはreadObjectメソッドの同期変更を忘れると、シーケンス化と逆シーケンス化のデータ処理方式が一致しない可能性があります.
        デフォルトのシーケンス化についてさらに説明する必要があるのは、1つ以上のドメインフィールドがtransientとマークされている場合、逆シーケンス化を行う場合、オブジェクト参照ドメインがnullに設定され、数値ベースドメインのデフォルト値が0、booleanドメインのデフォルト値がfalseのようなタイプのデフォルト値に初期化されます.これらの値がtransientドメインで受け入れられない場合は、readObjectメソッドを指定する必要があります.まずdefaultReadObjectを呼び出し、transientドメインを許容可能な値に復元します.
        最後に、デフォルトのシーケンス化形式を使用するかどうかにかかわらず、オブジェクトのステータス全体を読み込む他の方法で同期を強制する場合は、次のコードを参照して、オブジェクトのシーケンス化で同期を強制する必要があります.
private synchronized void writeObject(ObjectOutputStream s) throws IOException{
    s.defaultWriteObject();
}

        要するに、クラスをシーケンス可能にすることを決定した場合、どのようなシーケンス化形式を採用すべきかをよく考えてください.デフォルトのシーケンス化形式は、デフォルトのシーケンス化形式がオブジェクトの論理状態を合理的に記述できる場合にのみ使用できます.そうでなければ、オブジェクトの状態を合理的に記述するカスタムシーケンス化形式を設計します.エラーのシーケンス化形式の選択は、クラスの複雑さとパフォーマンスに永続的に悪影響を及ぼします.
七十六、保護性の編纂readObject方法
        エントリ39には、可変プライベートDateドメインを含む可変日付範囲クラスが記載されている.このクラスは、コンストラクタおよびアクセスメソッドで保護されたコピーDateオブジェクトによって、制約条件と可変性を極力維持します.次のコードを参照してください.
public final class Period {
    private final Date start;
    private final Date end;
    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 new IllegalArgumentException();
    }
    public Date start() {
        return new Date(start.getTime());
    }
    public Date end() {
        return new Date(end.getTime());
    }
    public String toString() {
        return start + " - " + end;
    }
    ... ...
}

        このクラスをシーケンス化すると仮定します.このオブジェクトの物理的表現は論理的表現と完全に一致するため、デフォルトのシーケンス化形式を使用することができます.しかし、クラスの宣言に「implements Serializable」という文字を追加するだけでは、このクラスは重要な制約を保証しません.
        問題は、逆シーケンス化されたデータソースがクラスインスタンスの通常のシーケンス化から来ている場合、問題は発生しません.逆に、逆シーケンス化されたデータ・ソースは、偽造されたデータ・ストリームのセットから来ています.実際には、逆シーケンス化のメカニズムは、指定されたオブジェクトをルール化されたデータ・ストリームのセットからインスタンス化することです.Periodインスタンス・オブジェクトの内部制約が破壊される危険に直面しなければなりません.次のコードを参照してください.
public class BogusPeriod {
    private static final byte[] serializedForm = new byte[] {
        ... ... //             ,       end    start
    };

    public static void main(String[] args) {
        Period p = (Period)deserialize(serializedForm);
        System.out.println(p);
    }

    private static Object deserialize(byte[] sf) {
        try {
            InputStream is = new ByteArrayInputStream(sf);
            ObjectInputStream ois = new ObjectInputStream(is);
            return ois.readObject();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}

        上記のコードを実行すると、Periodの制約が破られ、endの日付がstartより早いことがわかります.この問題を修正するために、まずdefaultReadObjectを呼び出し、逆シーケンス化されたオブジェクトの有効性を確認するreadObjectメソッドをPeriodに提供することができる.チェックに失敗すると、InvalidObjectException異常が放出され、逆シーケンス化プロセスが正常に完了しません.
private void readObject(ObjectInputStream s)
        throws IOException,ClassNotFoundException {
    s.defaultReadObject();
    if (start.compareTo(end) > 0)
        throw new InvalidObjectException(start + " after " + end);
}

        上記の攻撃方式に加えて、シーケンス化データストリームを偽造することによって逆シーケンス化方法の信頼をだまし取るもう一つのより隠れた攻撃方式が存在する.データを偽造すると、プライベートドメインフィールドの参照が外部に保存され、オブジェクトインスタンスの逆シーケンス化に成功した後も、外部が内部データを操作できるため、危険は依然として存在します.このリスクを回避するにはどうすればいいですか?readObjectメソッドは実際には別の共通のコンストラクタに相当するため、コンストラクタはパラメータの有効性を確認し、必要に応じてパラメータを保護コピーする必要があるという同様の注意点にも注意する必要があります.以下の改訂されたreadObjectメソッドを参照してください.
private void readObject(ObjectInputStream s)
        throws IOException,ClassNotFoundException {
    s.defaultReadObject();
    //     copy
    start = new Date(start.getTime());
    end = new Date(end.getTime());
    if (start.compareTo(end) > 0)
        throw new InvalidObjectException(start + " after " + end);
}

        保護copyは必ず有効性検査の前に行うことに注意してください.ここでは、デフォルトのreadObjectメソッドが受け入れられるかどうかを決定するのに役立つ基本的なルールを示します.ルールは、オブジェクト内の各非transientドメインに対応する公有コンストラクタを追加し、パラメータの値にかかわらず、チェックを行わずに対応するドメインに保存します.このようなやり方が依然として受け入れられる場合、デフォルトのreadObjectは合理的であり、そうでなければ、明示的なreadObjectメソッドを提供する必要がある.
        非finalのシーケンス化可能クラスの場合、readObjectメソッドとコンストラクタの間には、直接呼び出しても間接呼び出しても上書き可能なメソッドを呼び出すことはできません.ルールに違反し、メソッドが上書きされた場合、上書きされたメソッドは、サブクラスのステータスが逆シーケンス化される前に実行されます.プログラムが失敗する可能性が高い.