More Related Content
More from Appresso Engineering Team
More from Appresso Engineering Team (15)
Effective Java 輪読会 項目74-75
- 2. 第11章シリアライズ
項目74 Serializable を注意して実装する
項目75 カスタムシリアライズ形式の使用を検討する
項目76 防御的にreadObject を書く
項目77 インスタンス制御に対しては、readResolve よりenum 型を選
ぶ
項目78 シリアライズされたインスタンスの代わりに、シリアライズ・プロキ
シを検討する
※ スペースの関係で、以下では一部の「シリアライズ」を「直列化」として表記
2
- 4. Serializable を実装すること
クラスのインスタンスをシリアライズ可能にすること
4
誤解:シリアライズ可能にするのは簡単で、大した努力はいらない
「クラス宣言にimplements Serializable を追加するだけでできる」
⇒ 簡単にできるけど、長期的なスパンではひどい目にあうことに...
Serializable を実装する際の主要コスト
一旦リリースされるとクラスの実装を変更する柔軟性が低下
バグやセキュリティホールの可能性が増大
新しいバージョンのクラスをリリースすることに関連するテストの負荷が増大
⇒ Serializable インタフェースの実装は軽く考えて決めることではない!
- 5. クラスの実装変更柔軟性への影響
シリアライズ可能=シリアライズ形式を公開API に含める
5
デフォルトだとシリアライズ形式は最初の内部表現に結びつける
パッケージプライベートとprivate のインスタンスフィールドも公開API に
⇒ フィールドへのアクセスを最小限にする実践(項目13)が無駄になる
⇒ 長期間付き合える高品質のシリアライズ形式を注意深く設計すべき(項目75,78)
シリアライズ可能性に伴う発展に対する制約の例
シリアルバージョンUID(ストリーム一意識別子)
明示的指定しなければ、自動生成される値が適用される
自動生成される値は実装のインタフェースやメンバからの影響を受ける
クラスの実装を変更するとその値が変更される
⇒ 明示的な宣言がないと互換性を失い、実行時にInvalidClassException
- 7. リリースに関連するテストの負荷
クラス互換性を維持するためのテストの組み合わせ
7
新しいリリースでシリアライズ⇒ 今までの古いリリースでディシリアライズ
今までの古いリリースでシリアライズ⇒ 新しいリリースでディシリアライズ
⇒ リリース数が増えると大変なことに...
互換性テストの内容
バイナリ互換性:バイナリ形式の互換性
セマンティック互換性:オブジェクトの意味の互換性
⇒ 自動化での構築ができない
互換性テストの必要性
変更量に依存、変更量が大きいほど、テストの必要性が増大
注意深く設計されたカスタムシリアライズ形式ではテスト必要性は減る
- 8. Serializable を実装すべきか
クラスを実装するごとに、コストと恩恵を評価する
シリアライズに依存したフレームワークでクラスが使用される場合
8
例えばオブジェクトの転送や永続化に関するフレームワーク
⇒ Serializable を実装することは必要
Serializable を実装必須の他のクラスのコンポーネントとして
⇒ Serializable を実装すると、クラスの使用を容易にする
一般則として
コレクションクラスや値クラスはSerializable を実装すべき
活動的な実体を表すクラスはSerializable をめったに実装すべきでない
- 9. Serializable を実装すべきか
継承のために設計されたクラス(インタフェースも基本的に同じ)
9
⇒ Serializable をめったに実装すべきでない
例外として、シリアライズに依存するフレームワークを使用する場合
内部クラス
クラス定義との対応が定義されず、デフォルトのシリアライズ形式は不明確
無名クラスやローカルクラスの名前
コンパイラ生成による人工的フィールド
エンクロージングインスタンス参照を保存用
外部スコープからローカル変数値を保存用
⇒ Serializable を実装すべきではない
ただしstatic のメンバークラスはSerializable 実装できる
- 10. 継承のために設計されたクラス
Serializable を実装するケース
シリアライズ可能かつ拡張可能で、
10
インスタンスフィールドを持つクラスを実装する際の注意事項
インスタンスフィールドがデフォルト値に初期化されると成り立たない不変式あるか?
クラスにそのような不変式がある場合は以下メソッドを追加
// 状態を持ち拡張可能・シリアライズ可能クラスに対するreadObjectNoData を追加
private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException(“Stream data required”);
【要確認】サブクラスインスタンスのディシリアライズ処理において、ディシリアライズし
ようとするオブジェクトが自クラスをスーパークラスとして列挙していない場合、フィール
ドを適切な値に設定するためにreadObjectNoData は呼び出される
要するに、バージョンの違うサブクラスのオブジェクトをディシリアライズする状況
【Java オブジェクト直列化仕様】3.5 readObjectNoData メソッド
【stackoverflow】Java: When to add readObjectNoData() during
serialization?
}
- 11. 継承のために設計されたクラス
Serializable を実装しないケース
パラメータなしコンストラクタの提供を検討する
11
⇒ サブクラスのシリアライズ可能の拡張性を残すように
不変式がすべて確立されるオブジェクトを生成するのが最善
不変式の確立にクライアントの情報提供が必要な場合は要注意
public abstract class AbstractFoo {
...
// オブジェクトの完全性を意識して、アトミック参照で初期化状態を管理
private final AtomicReference<State> init = new AtomicReference<>(State.NEW);
protected AbstractFoo(int x, int y) { initialize(x, y); }
// オブジェクトの生成手段のみ提供、別途初期化メソッドの呼び出しが必要
protected AbstractFoo() {}
// 未初期化時の初期化メソッド
protect final void initialize(int x, int y) { ... }
// 内部状態の正しさを保証する
private void checkInit() { ... }
...
}
- 12. まとめ
Serializable の実装の容易さは見かけ倒し
短期間の使い捨てのクラスでなければ、Serializable を実装するか
は真剣に決めるべき
Serializable を実装するかを決める場合、特に継承のために設計さ
れたクラスは注意を払うべき
サブクラスでSerializable を実装・禁止することの中間的な設計ポイント
12
は、アクセス可能なパラメタなしコンストラクタを提供
⇒ サブクラスがSerializable を実装することを許すが、強制はしない
- 14. デフォルト直列化形式の受け入れ
時間制約がある中でクラスを作成する場合、適切な方法として
最善のAPI 設計に努力を集中すべき
使い捨てのクラスで実装して、後のリリースで置き換える
⇒ シリアライズ可能なクラスのシリアライズ形式はあとのリリースにも影響
14
(特にデフォルトのシリアライズ形式)
デフォルトのシリアライズ形式を受け入れる?
受け入れる前に、適切かどうかを最初に検討しなくてはならない
判断基準:オブジェクトの論理表現として適切か
デフォルトのシリアライズ形式はオブジェクトの物理表現を符号化したもの
論理表現と物理表現が同じであれば、おそらく適切
適切であると判断したら
不変式やとセキュリティを保証するため、多くの場合はreadObject メソッドを提
供しなければならない
- 15. 適切でないデフォルト直列化形式の使用
デフォルト直列化形式の使用が適切でない例
StringList (p.286)
15
論理データ:文字列の列
物理表現:リンクリスト
適切でないデフォルトのシリアライズ形式の使用のデメリット
公開APIが現在の内部表現に永久拘束される
内部の実装が変わっても古い実装に関するコードは取り除くことできない
過剰な空間を消費する可能性がある
データを使いやすくするための仕組みの構造をシリアライズする価値はない
過剰な時間を消費する可能性がある
実装によってコストの高い検索を行うことになる場合も
スタックオーバーフローを起こす可能性がある
クラスの構造によって再帰的な検索がスタックオーバーフローを起こすかも
- 16. 適切な直列化形式を持つクラスの実装
実装例: StringList (p.286)
シリアライズ形式:リスト内の文字列数と、文字列自身
16
物理表現の詳細は取り除かれる
transient 修飾子でインスタントフィールドを標記
defaultWriteObject/defaultReadObject メソッドの呼び出し
すべてのインスタントフィールドがtransient ならば、技術的には呼び出し
を省くことは許されるが、推奨しない
後方互換性と前方互換性を保ちながら、あとのリリースでtransient でない
フィールドを追加可能
- 17. transient 識別子の使用
インスタントフィールドがデフォルト直列化形式から省かれることを示す
フィールドをtransient でないと決める前に、そのフィールドの値が、
オブジェクトの論理状態の一部であることを確認するべき
デフォルト直列化形式を使用した場合、transient と宣言したフィールド
はデフォルト値に初期化される
デフォルト値がそのフィールドに対して受け入れられない場合の対処
17
受け入れられる値を回復させるreadObject メソッドを提供(項目76)
そのフィールドが最初に使用される時に遅延初期化を行う(項目71)
- 18. シリアライズ形式と関係ない注意事項
オブジェクト全体の状態を読み出す他のメソッドに課す同期をオブジェクト
のシリアライズにも課さなければならない
例えば、すべてのメソッドを同期することでスレッド安全性を達成しているスレッ
18
ドセーフのオブジェクトで、デフォルト直列化形式を使用する場合
// デフォルトのシリアライズ形式を持つ同期されたクラス用のwriteObject
private synchronized void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
}
作成するすべてのシリアライズ可能なクラスに、明示的なシリアルバー
ジョンUID を宣言すべき
非互換性の原因の可能性になるようなシリアルバージョンUID を排除
(項目74)
実行時の生成するためのコストの高い計算を回避
- 19. シリアライズ形式のドキュメンテーション
@serial タグ
シリアライズ形式に含まれるフィールドにつける
private のフィールドであっても、public のAPI (シリアライズ形式)を定
19
義しているため、文書化しなければならない
ドキュメンテーションを特別のページ「直列化された形式」に掲載するよう、
Javadoc ユティリティに指示する
@serialData タグ
クラスのシリアライズ形式を定義するメソッドにつける
private のメソッドフィールドであっても、public のAPI (シリアライズ形
式)を定義しているため、文書化しなければならない
ドキュメンテーションを特別のページ「直列化された形式」に掲載するよう、
Javadoc ユティリティに指示する
- 20. まとめ
クラスをシリアライズ可能にすべきと決めた場合(項目74)
シリアライズ形式について真剣に考えるべき
オブジェクトの論理的状態を適切に記述している形式
デフォルトのシリアライズ形式は、そうである場合にだけ使用する
そうでなければ、カスタムシリアライズ形式を設計する
クラスのシリアライズ形式の設計するのに多くの時間を割くべき
クラスの公開されたメソッドを設計するときと同様(項目40)
誤ったシリアライズ形式を選択すると
20
⇒ クラスの複雑さとパフォーマンスに永久的で否定的な影響