読者です 読者をやめる 読者になる 読者になる

【Effective Java】項目75:カスタムシリアライズ形式の使用を検討する(前半)

Java Effective Java

シリアライズする方法には次の二つの種類があります。

何の考慮もせずにデフォルトシリアライズ形式を用いることは、互換性やパフォーマンスの点で非常に危険です。 なるべくカスタムシリアライズ形式を用いるべきです。

デフォルトシリアライズ形式

デフォルトシリアライズを利用するには単に Serializable を実装します。 あとは何もしなくても、フレームワークが自動的にそのクラスの内部表現を効率的に符号化します。

すなわち、デフォルトシリアライズではクラスに含まれるすべてのプロパティが自動的にシリアライズされます。 そのため、本来はシリアライズするべきではない、実装上の都合やキャッシュなどのプロパティをすべてシリアライズしてしまいます。

たとえば、名前と年齢を持つ人物を表すクラスを考えてみます。 このクラスでデフォルトシリアライズを利用するには、単に次のように Serializable を実装するだけです。

public class Person implements Serializable {
    /**
     * 名前。非 null。
     * @serial
     */
    private String name;
    /**
     * 年齢。0以上。
     * @serial
     */
    private int age;

    public Person(String name, int age) {
        if (name == null) {
            throw new IllegalArgumentException("name = null");
        }
        if (age < 0) {
            throw new IllegalArgumentException("age: " + age + " < 0");
        }

        this.name = name;
        this.age = age;
    }
}

ここで、シリアライズに関するいくつか注意点があります。

@serial タグ

name と age のフィールドは private ですが Javadocドキュメンテーションコメントが含まれています。 private プロパヒットであっても、シリアライズされてしまえば公開された API として扱うため、ドキュメンテーションコメントをつけるべきです。

@serial タグを使えば Javadoc が「シリアライズ形式の文書化」に関する特別ページに、そのプロパティを掲載してくれます。

readObject メソッド

デフォルトシリアライズでは readObject メソッドの実装は必須ではありません。

しかし、不変式とセキュリティを保証するために readObject メソッドは必ず実装するべきです。

たとえば、Person では name と age には次の不変式があります。

  • name は null
  • age は 0 以上

もし、シリアライズされたファイルを操作して、これらの不変式を破壊するようなデータが仕込まれた場合、readObject がなければそのままデシリアライズされてしまうからです。 これに関する詳細については項目76で扱います。

カスタムシリアライズ形式

カスタムシリアライズを用いるためには Serializable を実装することに加えて readObject メソッドと writeObject メソッドを実装します。

readObject メソッドと writeObject メソッドで、開発者がシリアライズするべきプロパティを選びます。

これによって、単にクラスの物理表現(実装)がシリアライズするのではなく、オブジェクトの論理表現(仕様)をシリアライズすることができるようになります。

たとえば文字列のリスト示すクラスの StringList があるとします(これはあくまで例なので通常は List を使うべきです)。

public class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
    ...(省略)....
}

物理表現と論理表現

このクラスは論理的に表現すれば「文字列のリスト」ですが、物理的(実装的)に表現すると「文字列の双方向リンクリスト」と考えられます。 そして、デフォルトシリアライズを用いると物理表現である「文字列の双方リンクリスト」としてシリアライズされてしまいます。

これには次の4つの欠点があります。

  • 物理表現(実装)が公開 API となり、永久にサポートする必要がある
  • 多くの空間を消費する可能性がある
  • 多くの時間を消費する可能性がある
  • スタックオーバーフローを起こす可能性がある

一方、カスタムシリアライズを利用してクラスの論理表現をシリアライズすれば、これらの問題を回避し、よりシンプルにシリアライズすることができます。 論理的には StringList は論理的「リスト中の文字列数」と「文字列自身」を記憶しておけば十分だからです。

カスタムシリアライズを実装した StringList は次のようになります。

public class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }

    /**
     * この{@code StringList}インスタンスをシリアライズする。
     *
     * @serialData リストのサイズ({@code int})を書き出して、
     * 適切な順番にすべての要素({@code String})が続くようにする
     */
    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 num = s.readInt();
        for (int i = 0; i < num; i++) {
            add((String) s.readObject());
        }

    }
}

このカスタムシリアライズの実装にはいくつか注意点があります。 次回の記事でその注意点について述べます。

感想

長いので分割。。。 これを含めてあと4つか~。