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

【Effective Java】項目74:Serializable を注意して実装する

Effective Java Java

項目74からはオブジェクトシリアライズ API について説明します

オブジェクトをバイト列として符号化することをシリアライズバイト列からオブジェクトに復号化することをデシリアライズと呼びます。

シリアライズされたオブジェクトは仮想マシン間やネットワークをまたいで転送できるようになります。 また、ファイルに保存できるようになります。

Serializable

Java でクラスをシリアライズ可能にするには Serializable インタフェースを implements します。

一般的に、コレクションクラスや値クラスは Serializable を実装しても問題ありません。 しかし、スレッドプールなどの処理をまとめたクラスは Serializable を実装するべきではありません。

やるべき作業は単に Serializable インタフェースを implements するだけですが、その簡単さとは裏腹にいくつか考慮しなければならない点があります。

柔軟性の低下

Serializable を実装したクラスを一旦リリースしてしまうと、その実装を変更することは容易ではありません。

クラスをシリアライズ可能にすると、そのシリアライズ形式がクラスの公開 API の一部となります。 既存のシリアライズ形式を永久にサポートし続ける必要がでてきます。

カスタムシリアライズ形式を設計せずにデフォルトのシリアライズ形式を利用すると、シリアライズ形式はクラスの内部表現と結びついてしまいます。 すなわち、デフォルトのシリアライズ形式では private インスタンスフィールドでさえも公開されてしまうことになります。

最も単純な問題例はシリアルバージョン UID と呼ばれるストリーム一意識別子に関する問題です。 Serializable を implements したクラスは必ずこの識別子を保持するようになります。 これを明示的に指定しない場合には次の情報からコンパイラが自動生成します。

  • クラス名
  • クラスが実装しているインタフェース名
  • public と protected のメンバ

もし、識別子を明示的に指定しない場合、単に便利なメソッドを一つ追加しただけでこの識別子は変更されてしまいます。 結果として、互換性が破壊されてしまいます。

セキュリティ

二つ目に、シリアライズ可能にすることでバグやセキュリティホールが入り込む可能性を増大させるという問題があります。

シリアライズ時のオブジェクトは言語外の仕組みを使い、通常のコンストラクタを迂回して生成されます。 すなわち、本来のコンストラクタによって保証される不変式がオブジェクト生成時には保証されません。

そのため、まずはデシリアライズ処理時に不変式を満たすように正しく実装する必要があります。 また、不変式を満たさない生成中のオブジェクト内部に外部からアクセスができないようにしなければなりません。

テスト負荷の増大

三つ目に、シリアライズ可能なクラスでは変更時のテストの工数が通常よりも増えます。

新たなリリースのインスタンスシリアライズし、古いリリースのデシリアライズ処理でインスタンスが復元できるを確認する必要があります。 また、逆に、古いリリースのインスタンスシリアライズし、新しいリリースのデシリアライズ処理で復元できるかも確認する必要があります。

これらのテストは単に新旧のインスタンス間でシリアライズ/デシリアライズできるかというバイナリ互換性に加えて、 動作が意図しているものかどうかというセマンティクス互換性も検査する必要があります。

これの負荷はカスタムシリアライズ形式を正しく設計することで軽減できますが、それを完全になくすことはできません。

継承と Serializable

継承するために設計されたクラス(項目17)とインタフェースでは Serializable を拡張するべきではありません。

継承して利用するために設計されたクラスが Serializable を implements している場合、サブクラスの実装にかなりの手間がかかります。

しかし、継承のために設計されたクラスがシリアライズ可能でない場合、シリアライズ可能なサブクラスを書くことは不可能かもしれません。 シリアライズ可能なサブクラスを書くためには、親クラスにアクセス可能なパラメータ無しコンストラクタが必要です。

内部クラスと static メンバークラス

内部クラスは Serializable を実装するべきではありません。 内部クラスはそれを包含するエンクロージングインスタンスに対する参照や、外部スコープからのローカル変数の値を保持するためのコンパイラが生成するフィールドを使用しています。 これらに対するデフォルトのシリアライズ形式は未定義で不明瞭で、動作は保証されません。

一方、static メンバークラスはエンクロージングインスタンスに対する参照などを持っていません。 そのため、自由に Serializable を実装できます。