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