5.1 概要
Javaオブジェクトが、直列化を使って状態をファイルに保管したり、かたまりとしてデータベースに保管したりする場合、そのデータを読み込むクラスのバージョンがそのデータを書き込んだバージョンと異なる可能性があります。
バージョン管理には、クラスの同一性に関し、いくつかの根本的な問題があります。たとえば、互換性のある変更とは何か、という問題があります。 互換性のある変更とは、クラスとその呼出し元との間の規約に影響を与えない変更です。
このセクションでは、目標、前提条件、および解決策について記述します。この解決策は、変更できるものを制限し、メカニズムを慎重に選択することによって、この問題に対処しようとするものです。
ここで示す解決策では、フィールドの追加やクラスの追加によって展開するクラスを「自動的に」処理するメカニズムを示します。 直列化では、バージョン管理は、バージョンごとにクラス固有のメソッドを実装することなく行われます。 ストリーム形式は、クラス固有のメソッドを呼び出すことなく処理(トラバース)されます。
5.2 目標
目標は次のとおりです。
異なる仮想マシンで稼働する異なるバージョンのクラス間における双方向の通信を、次の方法でサポートする。
Javaクラスが、同じクラスの古いバージョンで書き込まれたストリームを読み込めるようなメカニズムを定義する。
Javaクラスが、同じクラスの古いバージョンで読み込まれることを意図したストリームを書き込めるようなメカニズムを定義する。
永続性とRMIのためのデフォルトの直列化を提供する。
RMIが直列化を使用できるように、パフォーマンスがよくシンプルなケースで簡潔なストリームを作成する。
ストリームを書き込むのに使われたクラスとまったく同じクラスを識別し、ロードできる。
バージョン管理しないクラスに対しオーバーヘッドを低く保つ。
ストリームに保管されているオブジェクト固有のメソッドを呼び出さずに、ストリームの処理(トラバーサル)が可能なストリーム形式を使用する。
5.3 前提
前提条件は次のとおりです。
バージョン管理は、目標を達成するためにストリーム形式を制御する必要があるので、直列化可能クラスだけに適用される。 外部化可能クラスは、外部形式に結合されるそれ独自のバージョン管理を行う。
すべてのデータやオブジェクトは、書き込まれた順序でストリームから読み込まれたり、スキップされたりしなければいけない。
クラスは、個別に展開したり、スーパー・タイプやサブタイプと協調して展開する。
クラスは名前で識別される。 同じ名前の2つのクラスが異なるバージョンであったり、完全に異なるクラスであったりすることがある。この違いは、それぞれのインタフェースや、それぞれのインタフェースのハッシュを比較すれば区別できる。
デフォルトの直列化では、型の変換は行われない。
ストリーム形式では、線形順序の型変更だけをサポートすればよく、型の任意の分岐をサポートする必要はない。
5.4 ストリームのバージョン管理はだれが行うか
クラスの展開において、非展開クラスによって設定された規約を維持するのは、展開された(後のバージョンの)クラスの責任です。 これは、2つの形を取ります。 まず、展開されたクラスは、元のバージョンによって与えられたインタフェースに関する既存の前提条件を壊すことはできません。それによって、展開されたクラスを元のクラスのかわりに使用できます。 次に、元の(または前の)バージョンと通信するとき、展開されたクラスは、以前のバージョンが非展開クラスの規約を引き続き満たせるだけの、十分で同等な情報を与える必要があります。
ここで説明した目的のために、各クラスは、そのスーパー・タイプによって定義されたインタフェースまたは規約を実装し、拡張します。 クラスの新しいバージョン、たとえば、foo'
は、foo
のための規約を維持する必要があり、インタフェースを拡張したり、その実装を修正したりできます。
直列化を介したオブジェクト間の通信は、それらのインタフェースによって定義される規約には含まれていません。 直列化は、実装間のprivateなプロトコルです。 各実装がそのクライアントによって期待される規約に従うように十分なやりとりをすることは、その実装の責任です。
5.5 互換性のあるJavaの型展開
Java言語仕様に、Javaクラスが展開するときのバイナリ互換の説明があります。 バイナリ互換の柔軟性のほとんどは、クラス、インタフェース、フィールド、メソッドなどの名前のシンボリック参照を、遅い段階でバインドすることに起因しています。
直列化されたオブジェクト・ストリームのバージョン管理を設計する場合の基本的な項目を、次に示します。
デフォルトの直列化メカニズムは、ストリームのフィールドと、仮想マシンの対応するクラスのフィールドとをバインドするのにシンボリック・モデルを使用します。
ストリーム内で参照される各クラスは、自らのクラス、そのスーパー・タイプ、およびストリームに書き込まれる各直列化可能フィールドの型と名前を一意に識別します。 フィールドの順序は、まずプリミティブ型のフィールドがフィールド名でソートされ、次にオブジェクト・フィールドがフィールド名でソートされて決定されます。
各クラスのストリームに出現するデータは、必須データ(オブジェクトの直列化可能フィールドに直接対応する)とオプション・データ(プリミティブやオブジェクトの任意のシーケンスで構成される)の2種類に分けられます。 ストリーム形式は、必須データおよびオプション・データがストリーム内でどのように発生するかを定義します。この定義により、必要に応じてクラス全体、必須データまたはオプション・データをスキップすることが可能です。
必須データは、クラス記述子で定義された順序でソートされた、オブジェクトのフィールドで構成されます。
オプション・データは、ストリームに書き込まれ、クラスのフィールドに直接対応しません。 クラス自体は、オプション情報の長さ、型、およびバージョン管理を担当します。
クラスに定義されると、
writeObject
/readObject
メソッドはクラスの状態を読み込み/書き込みするためのデフォルトのメカニズムに取って代わります。 これらのメソッドは、クラスのオプション・データの読み取りおよび書込みを実行します。 必須データの書込みはdefaultWriteObject
の呼出しを介して、必須データの読取りはdefaultReadObject
の呼出しを介して行われます。各クラスのストリーム形式の識別は、ストリーム固有識別子(SUID)を使って行われます。 デフォルトでは、ストリーム固有識別子は、クラスのハッシュです。 以降のバージョンのクラスでは、すべて、互換性のあるストリーム固有識別子(SUID)を宣言する必要があります。 これにより、同じ名前を持つ複数のクラスを、不注意で単一のクラスのバージョンとみなしてしまうことを防げます。
ObjectOutputStream
およびObjectInputStream
のサブタイプには、annotateClass
メソッドを使って、クラスを識別する独自の情報を含めることができます。たとえば、MarshalOutputStream
はクラスのURLを埋め込んでいます。
5.6 直列化に影響する型変更
この概念を使えば、展開するクラスのさまざまなケースに対し、設計上どのように対応するかを説明できます。 これらのケースは、クラスのどれかのバージョンによって書き込まれたストリームの観点から記述されます。 ストリームが同じクラスの同じバージョンで読み込まれた場合には、情報や機能が失われることはありません。 ストリームは、元のクラスに関する唯一の情報源です。 そのクラス記述は、それが元のクラス記述のサブセットであるかぎり、そのストリームのデータと、再構成されるクラスのバージョンを一致させるのに十分な情報です。
これらの記述は、クラスの以前のバージョンか以後のバージョンを再構成するためにストリームを読み込む、という観点からのものです。 RPCシステムの用語でいえば、これは「受け取り側が正しくする」システムです。 書込み側は、そのデータをもっとも適した形式で書き込むので、受け取り側は、その情報を解釈して必要な部分を抽出し、入手できない部分を補う必要があります。
5.6.1 互換性のない変更
クラスに対する互換性のない変更とは、相互運用性の保証が維持できないような変更です。 クラスの展開の過程で起こる互換性のない変更には、次のものがあります。
フィールドを削除する - クラスのフィールドが削除されると、書き込まれたストリームにはその値がない。 そのストリームが以前のクラスによって読み込まれると、ストリームに値がないため、そのフィールドの値はデフォルト値に設定される。 しかし、このデフォルト値は、以前のバージョンがその規約を果たす能力を損なうことがある。
階層においてクラスを上方または下方に移動する - ストリームのデータ順序が正しくなくなるため、この変更はできない。
非staticフィールドをstaticに、または非transientフィールドをtransientに変更する - デフォルトの直列化を前提としている場合、この変更は、フィールドをクラスから削除するのと同じことである。 そのクラスのこのバージョンでは、そのデータはストリームに書き込まれないので、そのクラスの以前のバージョンで読むことはできない。 フィールドの削除と同じように、以前のバージョンのフィールドはデフォルト値に初期化されるので、そのクラスは予期できないエラーとなることがある。
プリミティブ・フィールドの宣言された型を変更する - クラスの各バージョンは、データをその宣言された型で書き込む。 ストリームのデータの型はフィールドの型と一致しないので、クラスの以前のバージョンがそのフィールドを読み込もうとするとエラーになる。
writeObject
またはreadObject
メソッドを、デフォルトのフィールド・データの書き込みまたは読込みを行わないように変更したり、前のバージョンが書き込みまたは読込みを行わなかった場合にその書き込みまたは読込みを行うように変更する。 デフォルトのフィールド・データがストリームにあるかないかは、一貫していなければいけない。クラスを
Serializable
からExternalizable
に変更したり、その反対を行なったりするのは、互換性のない変更である。こうすると、そのストリームに、使用できるクラスの実装と互換性のないデータが入ることになる。クラスを非enum型からenum型に変更したり、その反対を行なったりすること。そのストリームに、使用できるクラスの実装と互換性のないデータが入ることになるため。
Serializable
やExternalizable
を取り除くのは、互換性のない変更である。こうすると、書き込まれたときに、そのクラスの古いバージョンで必要なフィールドが除外されることになる。writeReplace
またはreadResolve
メソッドをクラスに追加することは、その動作がクラスの以前のバージョンと互換性がないオブジェクトを作成する場合は非互換である。
5.6.2 互換性のある変更
クラスへの互換性のある変更は、次のように処理されます。
フィールドの追加 - 再構成されるクラスにストリームにないフィールドがあると、オブジェクトのそのフィールドはその型に対するデフォルト値に初期化される。 クラス固有の初期化が必要な場合、そのクラスはreadObjectメソッドによって、そのフィールドをデフォルト値以外に初期化できる。
クラスの追加 - ストリームには、ストリームにおける各オブジェクトの型階層がある。 ストリームのこの階層と現在のクラスを比較すれば、追加のクラスがわかる。 ストリームには、そのオブジェクトを初期化するために使用できる情報はないので、そのクラスのフィールドはデフォルト値に初期化される。
クラスの削除 - ストリームのクラス階層と現在のクラスのクラス階層を比較すれば、クラスが削除されたことがわかる。 この場合、そのクラスに対応するフィールドとオブジェクトが、ストリームから読み取られる。 プリミティブ・フィールドは破棄されるが、削除されたクラスによって参照されるオブジェクトは作成される。こうするのは、それらがストリームの後のほうで参照される可能性があるからである。 ストリームがガベージ・コレクトされたり、リセットされたりするときに、それらはガベージ・コレクトされる。
writeObject
/readObject
メソッドの追加 - ストリームを読み込むバージョンにこれらのメソッドがある場合、デフォルトの直列化によってストリームに書き込まれた必須データは、通常どおりreadObject
によって読み込まれなければいけない。 このメソッドは、オプション・データを読み込む前に、まずdefaultReadObject
を呼び出す必要がある。writeObject
メソッドは、通常どおり、defaultWriteObject
を呼び出して必須データを書き込まなければならず、その後、オプション・データを書き込むことができる。writeObject
/readObject
メソッドの削除 - このストリームを読み込むクラスにこれらのメソッドがないと、必須データはデフォルトの初期化によって読み込まれ、オプション・データは破棄される。java.io.Serializable
の追加 - これは、型を追加するのと同じことである。 ストリームにはこのクラスに対する値がないので、そのフィールドは、デフォルト値に初期化される。 直列化不能クラスのサブクラス化をサポートするには、そのクラスのスーパー・タイプに引数なしのコンストラクタがあり、そのクラス自身がデフォルト値に初期化されなければいけない。 引数なしのコンストラクタがないと、InvalidClassException
がスローされる。フィールドへのアクセスを変更 - アクセス修飾子public、package、protected、privateを変更しても、直列化によってそれらのフィールドに値を代入できることには影響しない。
フィールドのstaticから非staticへ、またはtransientから非transientへの変更 - 直列化可能フィールドを計算するためにデフォルトの直列化に依存する場合、この変更は、フィールドをクラスに追加するのと同じことである。 新しいフィールドはストリームに書き込まれるが、その値はそれより前のクラスによって無視される。これは、直列化によってstaticやtransientのフィールドに値が代入されないためである。
レコード・コンポーネントの追加または削除 - 再構成されるレコード・オブジェクトにストリームで発生しないレコード・コンポーネントがある場合、そのレコード・クラスの正規コンストラクタには、そのタイプのデフォルト値が渡されます。 特定の初期化が必要な場合、コンストラクタはコンポーネントをデフォルト以外の値に初期化できます。 正規コンストラクタに渡されないストリーム・フィールド値は、事実上破棄されます。
通常のクラスからレコード・クラスへのクラスの変更 - 通常のクラスからレコード・クラスに変換し、デフォルトの直列化に依存しているクラスは、レコード・クラスに変更できます。 通常のクラスは、その直接的なスーパークラスとして
java.lang.Object
を持つ必要があります。それ以外の場合、そのスーパークラスに直列化可能な状態はありません。 レコード・クラス・コンポーネントの名前とタイプは、通常のクラス化可能フィールドの名前と型と一致している必要があります。 レコード・オブジェクトは、レコード・クラスの標準的なコンストラクタを使用して再構築されます。 正規コンストラクタが例外をスローする場合、たとえば不変のチェック中にInvalidObjectException
がスローされます。レコード・クラスから通常のクラスへのクラスの変更 - デフォルトの直列化に依存するレコード・クラスは、通常のクラスに変更できます。 通常のクラスでは明示的な
serialVersionUID
を宣言する必要があり、その値は前のレコード・クラスserialVersionUID
の値と同じです。前のレコード・クラスに明示的なserialVersionUID
宣言がない場合は、0L
になります。 通常のクラス直列化可能フィールドの名前と型は、前のレコード・クラス・コンポーネントの名前と型と一致する必要があります。