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