86.Serializableを実装するときは十分に気をつけよ
Serializableにするにあたってのコスト
Serializeにするのは秒でできるが、長期的には大きなコストとなるかもしれない。
Serializableを実装すると、一度リリースした実装クラスを変更する柔軟性が失われる
クラスがSerializableを実装するということは、バイトストリームエンコーディングされたもの(以降、serialized form)は公開されたAPIの一部となる。
一度クラスを公開したとなれば、永久にそれをサポートすることが求められる。
また、カスタムしていないデフォルトのserialized formでは、privateなフィールドも公開APIの一部となるので、アクセスできるフィールドを最小限にする(Item15)設計が無意味になってしまう。
デフォルトのselialized formを受け入れ、後でクラスの内部表現を変えたとすると、互換性のない変化が起こってしまう。
クライアントが古いバージョンのクラスを使ってインスタンスをシリアライズし、新しいバージョンのクラスでデシリアライズした場合には問題が起きてしまう。
変換を行うことは可能であるが、困難であり、コードが汚くなってしまう。
Serializableを実装するときは長く使うことを考えて、高品質なものを設計するように心掛ける必要がある(Item87,90)。
Serializableの実装において課される制限の一例として、serial version UIDというものが関わってくる。
全てのSerializableなクラスは、serial version UIDという自身を証明するための番号をもっている。(復元前後でクラスのバージョンが異なっていないかを識別する為のものらしい。)(http://www.ne.jp/asahi/hishidama/home/tech/java/serial.html)
static final longなフィールドとしてこいつを定義しなかった場合、実行時に自動的に割り振られる。
この値はクラス名、メンバー等に影響を受け、何か変えると、この値も変わってしまう。
UIDを定義せず、互換性も壊れてしまった場合、InvalidClassExceptionが実行時に発生してしまう。
Serializableを実装すると、バグ、セキュリティホールの可能性が増大する
デシリアライズは「みえないコンストラクタ」のようなものである。みえないがゆえに、コンストラクタによって確立された不変条件(invariant)を保証し、攻撃者に構築中のオブジェクトにアクセスさせないようにする必要があることを忘れがちである。
デフォルトのデシリアライゼーションメカニズムに頼っていると、容易にinvariantを破壊されたり、不正アクセスされたりすることになる。
Serializableを実装すると、新しいバージョンのクラスをリリースする際にかかる手間が増える
Serializableを実装したクラスが改定されると、新しいバージョンのインスタンスがシリアライズ可能で、古いほうのバージョンでデシリアライズ可能か、また、その逆も調べなければならない。
そのため、バージョン数とシリアライズ実装クラスの積に比例する形でテストケースが増える。
テストの必要量は、Serializable実装クラスの初版が注意深く設計されて入れば減らすことができる(Item87,90)。
Serializableの実装は気軽に決めてよいものではない
オブジェクトの移送や永続化にJavaのシリアライゼーションを使用しているフレームワークにクラスを組み込むためには、Serializableを実装することは不可欠である。
また、Serializableの実装によって、他のクラスからコンポーネントとして扱うことは容易となる。
しかし、前述の通り、Serializableの実装にはさまざまなコストがある。
クラスを設計する際には、それらを天秤にかける必要がある。
歴史的には、BigInteger,Instantクラスなどの値クラスはSerializableを実装するが、thread poolなどのアクティブなエンティティを表すクラスでは滅多に実装されない。
継承用に設計されたクラス、インターフェースは基本、Serializableを実装、継承すべきでない
このルールを破るとそのクラスのサブクラスとなるクラスに、多大な重荷を負わせることになる。
本ルールの例外として、関わるもの全てにSerializable実装を求めるフレームワークに存在するクラス、インターフェースはSerializableを実装することは理解できる。
ThrowableやComponentはSerializableを実装している。
ThrowableがSerializableを実装しているため、RMIはサーバーからクライアントに例外を飛ばすことができる。
ComponentがSerializableを実装しているため、GUIの要素は移送されたり、保存されたりする。SwingとAWTの全盛期でもこの機能はあまり使われていなかったが。
Serializableで拡張可能でフィールドを持つクラスを実装する場合には気を付ける
インスタンスフィールドに何らかのinvariantがある場合には、サブクラスにはfinalizeメソッドをoverrideさせないようにせねばならない。
そうしないとfinalizer attackの危険(Item8)がある。
また、クラスのinvariantが、インスタンスフィールドの初期化によって壊れてしまうような場合には、readObjectNoDataメソッドを加える必要がある。
private void readObjectNoData()
throws ObjectStreamException;
(この辺謎)
Serializableにしないとなったときの注意
継承用のクラスでSerializableにしないと選択したときに、シリアライザブルなサブクラスを書くのはより手間がかかる。
そのようなクラスの通常のデシリアライズでは、スーパークラスにアクセス可能な引数なしのコンストラクタが必要となる。
それが用意できない場合は、Serialization Proxy Pattern(Item90)を用いる。
インナークラスはSerializableを実装すべきでない
インナークラスは合成フィールドを使用する。
合成フィールドは、コンパイラーが生成するもので、エンクロージングインスタンスへの参照と、エンクロージングスコープからのローカル変数への参照を保持する。
合成フィールドに対するシリアライズの形式は不定なので、インナークラスでのSerializable実装は避けるべき。
ただし、staticなメンバークラスはSerializable実装可。