S-JIS[2003-09-07/2010-04-29] 変更履歴

直列化(serialize:シリアライズ)

Serializableインターフェースについて。


直列化とは

Javaの説明(Javadocとか)を見ていると、よく「直列化」「直列化可能」「直列化された形式」という言葉に遭遇する。
これはserializeの訳語みたいだけど、なんだか意味不明…。

VC++シリアライズと言えば、「ファイルへのデータ保存」「ファイルからのデータ読み込み」を指す。Javaも結局は同じような事みたい。

普通(表面上は見えないけれども)、インスタンス(オブジェクト)の中はポインタを使いまくっているため、そのままファイルに書き出すことが出来ない(そのまま読み込んでも使えない、と言うのが正しいか)。
そこで、読み書きできる形にデータを整形する必要がある。この事を指して直列化(あるいは整列化)と呼んでいるようだ。
ちなみに、ファイルへ書き込んで保存することを永続化などと大げさに呼んでいるようだ。

シリアライズは、ファイルへの保存以外では、RMIEJB等のリモート呼び出しで受け渡すクラスに対してよく使われる。[2004-06-12]
また、HTTPセッションに保存するオブジェクトもシリアライズ可能でなければならなかったりする。[2007-12-07]


シリアライズの仕様

シリアライズを行う場合、その対象となるクラス(書き出し/読み込みを行うインスタンスのクラス)はjava.io.Serializableインターフェースをimplementsする必要がある。
ただし、オーバーライドすべきメソッドは無い。シリアライズの入出力を実際に行うwriteObject()の中で「シリアライズ可能かどうか」を調べる為に、「if(○○ instanceof java.io.Serializable)」という判定として使っているだけな為。
オブジェクトがSerializableをimplementsしていない場合、そのオブジェクトをシリアライズしようとするとNotSerializableExceptionが発生する。

また、シリアライズするクラス内のフィールド(メンバー変数)は基本的な型(プリミティブ型)シリアライズ可能なクラスでなければならない。
フィールドのインスタンスがシリアライズ可能なクラスでない場合、NotSerializableExceptionが発生する。 (実行時にフィールドに入っているインスタンスのクラスがシリアライズ可能でなければならないのであって、フィールドの宣言(コンパイル時)のクラスは関係ない)

シリアライズ対象クラスのスーパークラス(親クラス)がシリアライズ可能でない場合、スーパークラスにデフォルトコンストラクター(引数の無いコンストラクター)が無いか、あるいはprivateになっていてアクセスできない場合、シリアライズできない。[2007-06-13]
アクセスできるデフォルトコンストラクターが無い場合、InvalidClassExceptionが発生する。
復元(読み込み)されるときは、シリアライズ可能クラスのコンストラクターは呼ばれないが、スーパークラスがシリアライズ可能でない場合は、そのデフォルトコンストラクターは呼ばれる
フィールドのインスタンスもこれに準ずる(“シリアライズ可能”なはずなので)。つまりフィールドのインスタンスのコンストラクターは呼ばれないが、その親クラスのコンストラクターは(シリアライズ可能でなければ)呼ばれる。

シリアライズ対象クラスがSerializableを実装しているのにスーパークラスがSerializableを実装していない場合、スーパークラスが持っている変数の内容は転送されない![2004-06-12]
そういうスーパークラスはデフォルトコンストラクターが呼ばれて初期化されるわけだから、当然だわなぁ…

→SunのSerializableインターフェースの要件


書き出し・読み込みの例

シリアライズを使ってファイルへ読み書きするには、以下のような方法をとる。[2007-05-02]

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ファイル名"));
	oos.writeObject(書き出すインスタンス);
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ファイル名"));
	クラス 読み込む変数 = (クラス)ois.readObject();

※この読み書きされるクラスがシリアライズ可能である必要がある。

読み書きされるクラスのシリアライズ・デシリアライズの独自実装方法


シリアルバージョンID

Serializableを実装したら、serialVersionUIDという定数を定義した方がいいらしい。[2007-06-11]

class Test implements Serializable {
	private static final long serialVersionUID = 8531245739641223373L;
}

Eclipse3.1以降では、Serializableを実装したのに この定数を定義しないと、「シリアライズ可能クラスはlong型のstatic final serialVersionUIDフィールドを宣言していません」という警告が出るようになった。
まぁ、Eclipseの場合はその修正の選択肢で「生成シリアル・バージョンIDの追加」を選べば計算してくれるけど。
あるいは、警告を抑制する方法もある。

この定数は、復元前後でクラスのバージョンが異なっていないかを識別する為のものらしい。
なので、クラス内のフィールドやメソッド(の名前や型)に変更があった場合は 計算し直すのが筋だと思われる。
が、「変更されない」あるいは「実際には受け渡しは行わない」という割り切りで常に固定値(1とか)を指定しておく手もないわけではない。


ファイル等に書き込んだ時と そこから読み込んだ時でserialVersionUIDの値が異なる場合、読み込み時点でInvalidClassExceptionが発生する。[2008-07-03]

書き込み時点でserialVersionUIDが無くて読み込み時点で定義されていた場合は、serialVersionUIDのチェックは行われない。

書き込み時点でserialVersionUIDが有って読み込み時点で無かった場合は、デフォルトのserialVersionUIDが計算され、その値と比較される。そして不一致なら当然例外が発生する。
このデフォルトのserialVersionUIDの計算方法はJREの実装依存らしい。つまり、読み込み側のJavaVMが異なるバージョンだったら違う計算結果になる可能性がある、ということ。
したがって、serialVersionUIDはきちんと定義しておくのが推奨されているらしい。


シリアライズの対象・非対象フィールドの指定方法

フィールドの定義にtransientを付けると、Serializableをimplementsしたクラスであっても そのフィールドはシリアライズの対象外になる。[2005-06-26]
受け渡しには使わないが一時的に使うフィールド等に利用する。

	public transient final String TEST = "abc";

また、staticなフィールドもシリアライズの対象外となる。[2007-06-11]
なぜなら、staticなフィールドの値は 該当クラスが存在しているJavaVM内で共通な為。
つまり復元する時に、staticフィールドは復元先VMのクラスの値がそのまま使われる。(これが勝手に書き換えられたら、ちょっと気まずいわな)

あと、シリアライズする対象のフィールドを明示的に指定する方法もあるんだそうだ。 これで指定したフィールドは、transientが付いていてもシリアライズ対象になるそうだが。

	/**
	 * @serialField フィールド名1 フィールド1の型 フィールド1の説明
	 * @serialField フィールド名2 フィールド2の型 フィールド2の説明
	 * @serialField …
	 */
	private static final ObjectStreamField[] serialPersistentFields = {
		new ObjectStreamField("フィールド名1", フィールド1の型.class),
		new ObjectStreamField("フィールド名2", フィールド2の型.class),
		…
	}

ただしstaticでない内部クラスには使えないらしい


独自シリアライズ(writeObject()・readObject())

SerializableをimplementsしたクラスにwriteObject()・readObject()メソッドを実装すると、シリアライズ時の処理を自分で定義する事が出来る。[2010-04-28]

writeObject()・readObject()はSerializableインターフェースに定義されているメソッドではないので、オーバーライドするものではない。(そもそも可視性はprivateだし!)

writeObject()・readObject()の中では、自分のクラスで定義しているフィールドだけを扱う。
親クラス(や、子クラス)については何もしなくてよい。(そもそも親クラスのprivateフィールドにはアクセスできないし)

Sunのドキュメントには、writeObject()内でdefaultWriteObject()またはwriteFields()を一度だけ呼び出すように書かれている。
(readObject()ではdefaultReadObject()またはreadFields()を呼び出す)
ただし、defaultWriteObject()およびdefaultReadObject()は、デフォルトの(writeObject()やreadObject()を実装しない場合の)シリアライズ・デシリアライズ処理を行うメソッドである。
つまり、defaultWriteObject()を呼んだ時点でフィールドの出力が行われるし、defaultReadObject()を呼んだ時点でフィールドの読み込みが行われる。

使い分けとしては、追加処理を書きたい場合はdefaultWriteObject()・defaultReadObject()を、
読み書きするフィールドの加工を行いたい場合や読み書き方法を自分で定義したい場合はwriteFields()・readFields()
使うのかな。

なお、シリアライズ対象フィールドにはJavadocタグ@serial、シリアライズ用メソッドには@serialDataを付けるらしい。
(あくまでJavadoc用なので、付けなくても動作に変わりは無いが。生成されるJavadocで「直列化した形式」という専用ページが作られるらしい)


defaultWriteObject()・defaultReadObject()を使う例

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializableSample1 extends 親クラス implements Serializable {
	private static final long serialVersionUID = 199035831519635924L;
	/** @serial */
	private int data1;

	/** @serial */
	private int data2;

	/** @serial */
	private transient int data3;

	private transient int data4;
	〜コンストラクターやセッター・ゲッターやtoString()〜
	/**
	 * シリアライズ.
	 *
	 * @serialData data3だけ別途出力する。
	 * @param stream	出力ストリーム
	 * @throws IOException
	 */
	private void writeObject(ObjectOutputStream stream) throws IOException {
		System.out.println("writeObject: " + this);
		stream.defaultWriteObject();

		stream.writeInt(data3);
	}
	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
		System.out.println("readObject: " + this);
		stream.defaultReadObject();
		System.out.println("readObject: " + this);

		data3 = stream.readInt();
	}
}

defaultReadObject()の前後で自分のインスタンスのフィールドの内容を出力してみると分かるが、
readObject()が呼ばれた時点で親クラスのフィールドの値はセットされている。(自分のクラスのフィールドも初期化されている)
そしてdefaultReadObject()を呼び出すと、自分のクラスのフィールドの値もセットされている。
(defaultWriteObject()で出力した値が読み込まれるのだろう。もちろん、transientを指定したフィールドは読み書きされない)

自分でストリームに対して出力・入力した値は、普通に扱える。つまりtransientを指定していようがいまいが関係ない。(上記のdata3への読み書き)

SunのドキュメントにはdefaultWriteObject()・defaultReadObject()を呼び出さなければならないと書かれているが、
自分でストリームに対して対象フィールドのwrite()・read()を行う場合は、呼び出さなくても一応動作はするようだ。(仕様の範囲外の動作ということになってしまうのだろうが)
まぁ、そういう事をしたい場合はwriteFields()・readFields()を使うべきなんだろう。

writeObject()を記述せずにreadObject()だけ実装する、あるいは逆にwriteObject()だけ書いてreadObject()を書かないことも可能。
書かれなかった側は暗黙にdefault系メソッドだけ呼ばれることになる。


writeFields()・readFields()を使う例

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializableSample2 extends 親クラス implements Serializable {
	private static final long serialVersionUID = -5914041410544285416L;
	/** @serial */
	private int data1;

	/** @serial */
	private int data2;

	/** @serial */
	private transient int data3;

	private transient int data4;
	〜コンストラクターやセッター・ゲッターやtoString()〜
	/**
	 * シリアライズ.
	 *
	 * @serialData data3も出力する。
	 * @param stream	出力ストリーム
	 * @throws IOException
	 */
	private void writeObject(ObjectOutputStream stream) throws IOException {
		System.out.println("writeObject: " + this);
		stream.putFields();
		stream.writeFields();

		stream.writeInt(data1);
		stream.writeInt(data2);
		stream.writeInt(data3);
	}
	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
		System.out.println("readObject: " + this);
		stream.readFields();

		data1 = stream.readInt();
		data2 = stream.readInt();
		data3 = stream.readInt();
	}
}

この方法を採る場合、当然transientは無視される。

Sunのドキュメントには(writeFields()への言及はあるが)putFields()について書かれていないが、事前に呼び出しておかないとwriteFields()で例外が発生する。

putFields()はObjectOutputStream.PutFieldを返すので、それに対してput()するという方法もある。[2010-04-29]

	private void writeObject(ObjectOutputStream stream) throws IOException {
		System.out.println("writeObject: " + this);
		ObjectOutputStream.PutField fields = stream.putFields();
		fields.put("data1", data1);
		fields.put("data2", data2);
		stream.writeFields();

		stream.writeInt(data3);
	}
	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
		System.out.println("readObject: " + this);
		ObjectInputStream.GetField fields = stream.readFields();
		data1 = fields.get("data1", (int)0);
		data2 = fields.get("data2", (int)0);

		data3 = stream.readInt();
	}
}

fieldsのput()やget()に指定する名前は、フィールド名。
したがって、存在しない名前を指定すると例外が発生する。また、transientフィールドも指定できない。

この方法を使う方が正当(本来想定された方法)なのだろうが、個人的には、フィールド名を文字列で指定するようなやり方は好きではない。
(Eclipseでのフィールドの使用箇所の検索やフィールド名のリファクタリングの対象にならないから)


Externalizable

シリアライズの独自実装方法としては、SerializableにwriteObject()・readObject()を実装する方法の他に、Externalizableインターフェースを実装する方法がある。[2010-04-28]

ExternalizableはSerializableを継承したインターフェースだが、writeExternal()・readExternal()メソッドが定義されている。
シリアライズ・デシリアライズ時にこれらのメソッドが呼ばれる。
writeExternal()・readExternal()では、(SerializableのwriteObject()・readObject()と異なり、)親クラスのフィールドについても自分で読み書きを実装しなければならない。

import java.io.Externalizable;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class ExternalizeSample implements Externalizable {
	/** @serial */
	private int data1;

	/** @serial */
	private transient int data2;
	/** デフォルトコンストラクター */
	public ExternalizeSample() {
		System.out.println("constructor: " + this);
	}

	public ExternalizeSample(int n1, int n2) {
		data1 = n1;
		data2 = n2;
	}

	@Override
	public String toString() {
		return "data1=" + data1 + ", data2=" + data2;
	}
	/**
	 * シリアライズ.
	 *
	 * @serialData data1,data2を出力する。
	 * @param out	出力ストリーム
	 * @throws IOException
	 */
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		System.out.println("writeExternal: " + this);

		out.writeInt(data1);
		out.writeInt(data2);
	}
	@Override
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		System.out.println("readExternal: " + this);

		data1 = in.readInt();
		data2 = in.readInt();
	}
}

ExternalizableはSerializableを継承しているくせに、serialVersionUIDを定義しなくてもいいようだ。(Eclipseで警告にならない)

また、デフォルトコンストラクター(引数なしのpublicコンストラクター)が必要。


シリアライズできないクラス

APIの大抵のクラスはシリアライズ可能になっているけれど、シリアライズできないクラスもある。例えばStreamやConnectionはシリアライズできない。
(InputStreamなんかがリモートで渡せると、ファイル入力とかをネットワーク経由で行うのと同じだから無理だわな)


シリアライズ指定の本来の姿?

前述の通り、Serializableインターフェースは「シリアライズできる」という印でしかない。[2007-12-07]
なので、アノテーションになっているのがあるべき姿ではないかと思う。

class Bean implements Serializable {
	private static final long serialVersionUID = 8531245739641223373L;

	〜
}

↓こんな感じ

@Serializable(8531245739641223373L)
class Bean {
	〜
}

serialPersistentFieldsなんかもSerializableアノテーションの属性として定義できるかも。 (→無理だった[2008-07-05]

アノテーションはJDK1.5で導入されたものであり、もちろんSerializableはその前からあったので、今さらアノテーションに変えたりはしないだろうけど。
あ、でもコンパイル時に「Serializableアノテーションを見つけたら今までの形式に変換する」ことは出来そうだなぁ…。
そうなると、serialVersionUIDもその時点で計算してくれる方が楽だなぁ……。


参考


Java目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま