87.カスタムserialized formの利用を考慮せよ
通常のserialized formを選択した場合、将来修正するということはできなくなると考えねばならない。
適切か否か考慮せずにデフォルトのserialized formを選択してはならない
一般に、カスタムserialized formを設計することを選んだ時のエンコーディング結果とほとんど変わらない場合の時だけ、デフォルトserialized formを選ぶべき。
デフォルトserialized formはオブジェクトグラフの物理表現を効率的にエンコーディングしたものである。
つまり、
デフォルトserialized formでいい場合
デフォルトserialized formはオブジェクトの物理表現と論理コンテンツが一致している場合に適切であることが多い。
例えば以下のような、シンプルに人の名前を表現したクラスは、デフォルトserialized formが向いている。
// Good candidate for default serialized form
public class Name implements Serializable {
/**
* must be non-null
* @serial
*/
private final String lastName;
/**
* must be non-null
* @serial
*/
private final String firstName;
/**
* Middle name, or null if there is none
* @serial
*/
private final String middleName;
public Name(String lastName, String firstName, String middleName) {
super();
this.lastName = lastName;
this.firstName = firstName;
this.middleName = middleName;
}
}
Nameのインスタンスフィールドが論理的内容と対応している。
デフォルトserialized formが適切だと判断した場合でも、不変条件やセキュリティを確保するためにreadObjectメソッドを提供せねばならないときもある。
上記のNameの場合であれば、lastNameとfirstNameがnullでないことを、readObjectメソッドが保証せねばならない。このことはItem88,90で詳細に述べる。
NameのlastName,firstName,middleNameはprivateなフィールドであるにもかかわらず、ドキュメントが付されている。
その理由は、これらのprivateフィールドは、クラスのserialized formとして公開されるからである。
@serialというタグをつけると、生成されるJavadocでserialized formに関する専用ページを作れる。
デフォルトserialized formが向いてない場合
Nameクラスとは真逆に位置するものとして、以下のような文字列のリストを表すクラスを考える。
// デフォルトserialized formにしてはならない!
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry {
String data;
Entry previous;
Entry next;
}
}
論理的には、このクラスは文字列のシーケンスを表している。物理的には、双方向リストとしてのシーケンスを表している。
デフォルトserialized formを受け入れるとすると、serialized formは、連結リストの全要素と、entry間の全リンクを反映したものとなる。
物理的な表現と論理的な内容の間で乖離がある場合に、デフォルトserialized formを使うことには4つのディスアドバンテージがある。
- 公開APIとその時の内部表現が、永続的に蜜結合となってしまう
- メモリ空間をめっちゃ食う
- 時間めっちゃ食う
- スタックオーバーフロー起きうる
(難しくてよくわからん)
改訂版StringList
StringListの合理的なserialized formは、数個の文字列で構成されたlistである。
この構成は、StringListの論理的なデータの表し方である。
以下がwriteObjectとreadObjectを使った、改訂版のStringListである。
package tryAny.effectiveJava;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
// 改良を加えたStringList
public final class StringList2 implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// No longer Serializable
private static class Entry {
String data;
Entry previous;
Entry next;
}
// listに文字列を加える
public final void add(String s) {
}
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 numElements = s.readInt();
for (int i = 0; i < numElements; i++) {
add((String) s.readObject());
}
}
}
上記のクラスにおいて、writeObjectはまずdefaultWriteObjectメソッドを呼び出し、readObjectはまずdefautlReadObjectメソッドを呼び出している。
クラスの全フィールドがtransientである場合には、defaultWriteObjectメソッド、defautlReadObjectメソッドを呼び出す必要がないといわれることがあるが、そんなことはなく、呼び出さねばならない。
これらの呼び出しがあることで、のちのリリースでtransientでないフィールドを、互換性を保ちながら追加することが可能となる。
性能面に関して、10文字の文字列が10個連なったリストで計測したとき、改訂版StringListは、以前のメモリ量の半分程度になり、シリアライズの速さも倍となり、スタックオーバーフローの心配もなくなった。
Objectの不変条件が実装と結びつかない場合
hashテーブルの物理的な実装は、hash bucketsの中にキーのハッシュ値が連なったものである。
キーのhash値は実装によって異なるので、デフォルトのserialized formを用いると深刻なバグを生む。
transient
デフォルトserialized formを利用するかどうかに関わらず、defaultWriteObject を呼び出すと transient 指定されたフィールド以外のフィールドはすべてシリアライズされる。不必要なシリアライズを避けるために、できるだけtransient修飾子をつけるようにする。
transientが付されたフィールドはデシリアライズされたときに、デフォルトの初期値になっている。つまり、参照型変数であればnull、intなどなら0、booleanであればfalseになっている。
それが許容できないのであれば、readObjectメソッドにて設定をするか(Item88)、使用時に遅延初期化する(Item83)。
他のメソッドで同期をとっている場合は、シリアライズ時も同期をとる
シリアライズ時も同期をとる必要があるので、writeObjectにもsynchronizedをつける必要がある。
serialVersionUIDを明示する
明示することによって、ソースの非互換を防ぐことができる(Item86)。また、serialVersionUIDを実行時に生成しなくてよいので、ちょっとだけ性能が良くなる。
新しくクラスを書く場合には、この番号は何でもよく、別に一意な値でなくともよい。
現存するクラスにUIDを付与し、互換性も保とうとする場合は、古いバージョンのUIDにしてやる必要がある。