Effective Java 第3版を読んだので、本書で紹介されていた全90項目のプラクティスを簡潔にまとめる。
https://www.maruzen-publishing.co.jp/item/?book_no=303408

本記事は第12章「シリアライズ」の項目について記載する。

第12章 シリアライズ

項目85 Javaのシリアライズよりも代替手段を選ぶ

デシリアライズするデータが信用できない場合、デシリアライズはするべきではない。

信頼できないストリームのデシリアライズは、リモートコード実行、DoS攻撃などの攻撃に脆弱である。

もしデシリアライズが必要であれば、デシリアライズフィルターを使うことでデシリアライズする前に検査することができる。

また、シリアライズする際には、JSONやprotobufといったクロスプラットフォームの構造化データ表現を使うべきである。

項目86 Serializableを細心の注意を払って実装する

クラスのインスタンスをシリアライズ可能とするには、クラス宣言にimplements Serializableを追加するだけである。

ただ、適切に使うには長期的なコストがかかることを留意しなければならない。

  • 公開APIのクラスの実装を変更する柔軟性を低下させる。
  • バグやセキュリティホールの可能性を増大させる。
  • 新しいバージョンのクラスのリリースに関連したテストの負荷を増大させる。

よって、Serializableインタフェースを実装することは、慎重に判断する必要がある。

項目87 カスタムシリアライズ形式の使用を検討する

クラスのシリアライズ化において、適切かどうかの検討をせずにデフォルトのシリアライズ形式を採用するべきではない。
 
デフォルトのシリアライズ形式を使うことで、次のような短所が出てくる。

  • 公開APIが現在の内部表現に永久に拘束される。
  • 過剰な空間(サイズ)を消費する可能性がある。
  • 過剰な時間を消費する可能性がある。
  • スタックオーバーフローを起こす可能性がある。

デフォルトのシリアライズ形式は、オブジェクトの論理的状態を適切に記述している場合にだけ使うべきである。

そうでなければ、オブジェクトを適切に記載するカスタムシリアライズ形式を設計すること。

項目88 防御的にreadObjectメソッドを書く

readObjectメソッドを明示的に実装する際には、インスタンスを生成するようなpublicなコンストラクタを書くのと同義であると考えるべきだ。

readObjectメソッドを実装するときのガイドラインは次のとおり。

  • privateなフィールドに保存されるオブジェクトは、防御的コピーに努める。
  • 値の検査をして検査に問題があれば、InvalidObjectExceptionをスローさせる。また、値の検査は防御的コピーの後に行うべき。
  • readObjectメソッドから、クラス内のオーバーライド可能なメソッドを呼び出してはならない。

項目89 インスタンス制御に対しては、readResolveよりもenum型を選ぶ

次のようなシングルトンクラスをシリアライズするために、implements Serializableを追加するとこのクラスはシングルトンではなくなる。

public class Elvis {
  public static final Elvis INSTANCE = new Elvis();
  private Elvis() {}
}

なぜなら、readObjectメソッドは新たに生成されたインスタンスを返却するからである。

シングルトン性質を保持するためには、readResolveメソッドを使用し、readObjectメソッドで生成されたインスタンスを他のインスタンスに置換させる。

private Object readResolve() {
    return INSTANCE;
}

インスタンス制御のためにreadResolveメソッドを実装するのであれば、すべてのインスタンスはtransientと宣言されなければならない。

宣言しない場合、readResolveメソッドが実行される前に、ディシリアライズされたオブジェクトへの参照がされてしまう。(Stealer攻撃)

ただ、インスタンス制御の不変式を強制するだけであれば、enumシングルトンを使うべきである。

enumシングルトンを使うことで、定数の他にインスタンスが存在しないことをJavaが保証してくれる。

public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs = {"hoge", "huga"}; 
}

ただし、コンパイル時にインスタンスが把握できない場合は、そのクラスをenumとして表現することができない。

そういった状況のみ、readResolveメソッドを提供してインスタンス制御を可能とさせる。

項目90 シリアライズされたインスタンスの代わりに、シリアライズ・プロキシを検討する

Serializableを実装することは、バグとセキュリティの問題を増大させる。

それらのリスクを減らす手法が、シリアライズ・プロキシ・パターン である。

シリアライズ・プロキシ・パターンは、シリアライズ可能なクラスのprivate staticのネストしたクラスを設計する。

実装するコンストラクタは、引数からフィールドに値をコピーするだけにして、一貫性検査や防御的コピーをする必要はない。

private static class SerializationProxy implements Serializable {
    private final Date start;
    private final Date end;

    SerializationProxy(Period p) {
        this.start = p.start;
        this.end = p.end;
    }

    private static final long serialVersionUID = 423141234L;
}

次に、writeReplaceメソッドを追加する。
このメソッドは、エンクロージングクラスのインスタンスの代わりに、SerializationProxyインスタンスを出力する。

private Object writeReplace() {
    return new SerializationProxy(this);
}

また、クラスの不変式を破壊するような攻撃を防ぐために、次のようなreadObjectメソッドを追加する。

private void readObject(ObjectInputStream stream) 
    throws InvalidObjectException {
    throw new InvalidObjectException("Proxy required");
}

最後に、エンクロージングクラスの論理的に等価なインスタンスを返却するreadResolveメソッドをSerializationProxyに追加する。
このメソッドは、シリアライズシステムがディシリアライズの際にシリアライズ・プロキシをエンクロージングクラスのインスタンスへ変換する。

private Object readResolve() {
    return new Period(start, end);
}