11.1 オブジェクトの保存とtransient修飾子
11.1.1 オブジェクトの直列化
コンストラクタによって初期化されたオブジェクトはプログラムが実行されていなければ存在することができません。オブジェクトを直接ディスクに保存することはできないため、ある状態のオブジェクトを再現したければ、それに必要な情報をディスクに保存しておく必要があります。データとして保存できるのは、最終的にはプリミティブな値です。参照型であるオブジェクトを直接保存することはできません。
しかし、ネットワーク上の他の仮想マシンや、他のプロセスとオブジェクトデータを交換したり、永続的なオブジェクトを作るために、わざわざ独自の方法でバイナリ化するのは面倒です。そこで、Java では標準ライブラリの java.io パッケージに、オブジェクトをバイト配列に変換する方法を定め、それに従ったクラスを公開しています。このように、オブジェクト指向のオブジェクトをバイト配列に変換することを直列化と呼びます。直列化を用いれば、ネットワークやプロセスの間でオブジェクトデータを交換することが容易になります。
直列化と言っても方法は単純で、フィールドの値をバイト配列に変換するだけです。フィールドの型が参照型である場合、そのオブジェクトも直列化します。復元する時は、逆の順序でフィールドの値を復元します。しかし、このような方法で直列化しているため、オブジェクトによっては保存や再生に何らかの問題が発生してしまう可能性があります。例えば、システムに動的にアクセスしているオブジェクトを直列化すると、その後に復元してもオブジェクトが保有しているシステム情報はすでに使えなくなっている可能性があります。そのため、すべてのオブジェクトが安全に直列化できるとは限らないのです。各々のクラスが独自の方法でデータを直列化する方法も定められていますが、直列化の詳細については言語仕様の範囲ではないので本書では説明できません。詳しくは java.io.Serializable インタフェースを参照してください。この場では、オブジェクトをバイト配列に変換することができるという事実を知っていればそれで良いでしょう。
オブジェクトを直列化するには java.io.ObjectOutputStream クラスを利用します。このクラスは OutputStream クラスを継承しているので、バイト配列を出力する基本的な write() メソッドが使えるほかに、オブジェクトを書き込む writeObject() メソッドなどが拡張されています(表1)。
コンストラクタ | 解説 |
---|---|
ObjectOutputStream(OutputStream out) | 指定された OutputStream に書き込む ObjectOutputStream を作成します。 |
メソッド | |
void defaultWriteObject() | 現在のクラスの非 static フィールドと非 transient フィールドを、ストリームに書き込みます。このメソッドを呼び出すことができるのは、直列化が行われているクラスの writeObject メソッドだけです。 |
ObjectOutputStream.PutField putFields() | ストリームに書き込まれる持続フィールドをバッファに格納するために使用されるオブジェクトを取得します。 |
void reset() | ストリームにすでに書き込まれているオブジェクトの状態を無効にします。 |
void useProtocolVersion(int version) | ストリームの書き込み時に使用するストリームプロトコルのバージョンを指定します。 |
void writeBoolean(boolean val) | boolean を書き込みます。 |
void writeByte(int val) | 8 ビットのバイトを書き込みます。 |
void writeBytes(String str) | String をバイトの列として書き込みます |
void writeChar(int val) | 16 ビットの char を書き込みます。 |
void writeChars(String str) | String を char の列として書き込みます。 |
void writeDouble(double val) | 64 ビットの double を書き込みます。 |
void writeFields() | バッファに格納されたフィールドをストリームに書き込みます。 |
void writeFloat(float val) | 32 ビットの float を書き込みます。 |
void writeInt(int val) | 32 ビットの int を書き込みます。 |
void writeLong(long val) | 64 ビットの long を書き込みます。 |
void writeObject(Object obj) | 指定されたオブジェクトを ObjectOutputStream に書き込みます。 |
void writeShort(int val) | 16 ビットの short を書き込みます。 |
void writeUnshared(Object obj) | ObjectOutputStream に「共有されない」オブジェクトを書き込みます。 |
void writeUTF(String str) | この String のプリミティブデータを UTF 形式で書き込みます。 |
バイト配列に変換されたオブジェクトは java.io.ObjectInputStream クラスでオブジェクトに復元することができます。このクラスは InputStream クラスを継承しているので、read() メソッドなどを使えるほかに、readObject() など、ObjectOutputStream に対応する読み込みメソッドを提供しています(表2)。
コンストラクタ | 解説 |
---|---|
ObjectInputStream(InputStream in) | 指定された InputStream から読み込む ObjectInputStream を作成します。 |
メソッド | |
int available() | ブロックせずに読み込むことができるバイト数を返します。 |
void defaultReadObject() | 現在のクラスの非 static および非 transient のフィールドを、このストリームから読み込みます。 このメソッドを呼び出すことができるのは、直列化を復元されているクラスの readObject メソッドだけです。 |
boolean readBoolean() | boolean を読み込みます。 |
byte readByte() | 8 ビットのバイトを読み込みます。 |
char readChar() | 16 ビットの char を読み込みます。 |
double readDouble() | 64 ビットの double を読み込みます。 |
ObjectInputStream.GetField readFields() | ストリームから持続フィールドを読み込み、それらを名前を指定してアクセスできるようにします。 |
float readFloat() | 32 ビットの float を読み込みます。 |
void readFully(byte[] buf) | バイトを読み込みます。 |
void readFully(byte[] buf, int off, int len) | バイトを読み込みます。 |
int readInt() | 32 ビットの int を読み込みます。 |
String readLine() | 推奨されていません。 このメソッドはバイトを正確に文字に変換しません。詳細および代替メソッドについては DataInputStream を参照してください。 |
long readLong() | 64 ビットの long を読み込みます。 |
Object readObject() | ObjectInputStream からオブジェクトを読み込みます。 |
short readShort() | 16 ビットの short を読み込みます。 |
Object readUnshared() | ObjectInputStream から「共有されない」オブジェクトを読み込みます。 |
int readUnsignedByte() | 符号なし 8 ビットバイトを読み込みます。 |
int readUnsignedShort() | 符号なし 16 ビットの short を読み込みます。 |
String readUTF() | UTF 形式の文字列を読み込みます。 |
void registerValidation(ObjectInputValidation obj, int prio) | オブジェクトグラフが返される前に検証されるべきオブジェクトを登録します。 |
int skipBytes(int len) | バイトをスキップします。 |
ObjectOutputStream クラスの writeObject() メソッドでオブジェクトを書き込み、ObjectInputStream クラスの readObject() メソッドでオブジェクトを読み込むことができるようになります。バイト配列に変換されたデータはストリームの概念に従ってファイルに保存しても、ネットに送り出しても、メモリに常駐させてもかまいません。readObject() メソッドが返すのは Object 型なので、読み込んだオブジェクトを適切な型に変換する必要があります。
ObjectOutputStream クラスは複数の異なる型のオブジェクトやデータを出力することができます。この場合は、オブジェクトを書き込んだ順序と同じ順番で読み込めば、正しく再現することができます。
ただし、先ほども説明したように、すべてのオブジェクトが安全に直列化できるわけではありません。そこで、直列化を想定しているクラスは Serializable インタフェースを実装しなければなりません。Serializable を実装しないオブジェクトを writeObject() で出力しようとした場合、java.io.NotSerializableException 例外が発生してしまいます。
Serializable インタフェースはメンバを宣言していないので、実装しなければならないメソッドはありません。このインタフェースは、クラスが安全に直列化することができることをアピールする手段として提供されています。
import java.io.*; class Point implements Serializable { public final int x , y; public Point(int x , int y) { this.x = x; this.y = y; } public String toString() { return "X=" + x + ",Y=" + y; } } class Test { public static void main(String args[]) throws Exception { File file = new File(args[0]); if (file.exists()) { //オブジェクトを読み込む FileInputStream fin = new FileInputStream(file); ObjectInputStream oin = new ObjectInputStream(fin); Point pt = (Point)oin.readObject(); oin.close(); fin.close(); System.out.println(pt + ":オブジェクトを読み込みました"); } else { //オブジェクトを書き込む FileOutputStream fout = new FileOutputStream(file); ObjectOutputStream oout = new ObjectOutputStream(fout); Point pt = new Point(400 , 300); oout.writeObject(pt); oout.close(); fout.close(); System.out.println(pt + ":オブジェクトを書き込みました"); } } }
>java Test test.obj X=400,Y=300:オブジェクトを書き込みました >java Test test.obj X=400,Y=300:オブジェクトを読み込みました
コード1は、座標を表す Point クラスのオブジェクトを直列化して、コマンドライン引数で指定したファイルに Point オブジェクトを保存するプログラムです。指定した引数のファイルがすでにディスクに存在する場合は、そのファイルからオブジェクトを読み込んで、オブジェクトの情報を表示します。
オブジェクトの直列化には、ObjectOutputStream クラスを用いて、生成した pt オブジェクトを writeObject() メソッドから出力することで実現しています。これで、x フィールドに 400、y フィールドに 300 を保存する Point オブジェクトを永続的に保存することができます。
直列化された Point クラスのオブジェクトを復元するには、ObjectInputStream クラスの readObject() メソッドを用います。readObject() メソッドは入力ストリームの現在の位置から直列化されたオブジェクトを読み込んで返します。実行結果を見れば、保存した Point オブジェクトと同じ値の Point オブジェクトが生成されていることがわかります。
11.1.2 直列化の拒否
もし、直列化したいクラスのフィールドに直列化できない型のオブジェクトが存在すると、そのクラスは直列化することができません。例えば、入出力ストリームのオブジェクトなどは永続化することができないため、システムの状況に依存するオブジェクトは直列化するべきではないと考えられます。
このようなクラスの要求に柔軟にこたえられるように、Serializable インタフェースを実装するクラスが独自の方法で直列化、及び復元できるカスタマイズ方法が与えられていますが、これは少し面倒です。独自の方法で直列化、復元するにはクラスに次の 2 つのメソッドを実装させます。
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
writeObject() メソッドは out パラメータに実装するクラスの状態を書き込む責任があり、readObject() メソッドは in パラメータから情報を読み込んでフィールドを復元する責任を持ちます。これらのメソッドはコンパイル時に呼び出すのではなく、ObjectOutputStream や ObjectInputStream クラスで、直列化や復元を行うときに動的に調べられ、メソッドを実装していれば自動的に呼び出される仕組みになっています。どのようにクラスの情報を保存、復元するかは、このメソッドを実装するクラスに依存します。
これは、どのようなクラスでも柔軟に直列化させることができるため、極めて興味深い方法です。しかし、特定のフィールドの直列化を避けたい程度であれば、このような独自の直列化コードを記述する必要はありません。フィールドの宣言に transient 修飾子を指定すれば、そのフィールドは直列化されません。オブジェクトの保存や復元の時は、そのフィールドだけが無視されます。
import java.io.*; class Point implements Serializable { public final int x; public transient final int y; public Point(int x , int y) { this.x = x; this.y = y; } public String toString() { return "X=" + x + ",Y=" + y; } } class Test { public static void main(String args[]) throws Exception { File file = new File(args[0]); if (file.exists()) { FileInputStream fin = new FileInputStream(file); ObjectInputStream oin = new ObjectInputStream(fin); System.out.println(oin.readObject() + ":オブジェクトを読み込みました"); oin.close(); fin.close(); } else { FileOutputStream fout = new FileOutputStream(file); ObjectOutputStream oout = new ObjectOutputStream(fout); Point pt = new Point(400 , 300); oout.writeObject(pt); oout.close(); fout.close(); System.out.println(pt + ":オブジェクトを書き込みました"); } } }
>java Test test.obj X=400,Y=300:オブジェクトを書き込みました >java Test test.obj X=400,Y=0:オブジェクトを読み込みました
コード2は コード1を改良した、transient 修飾子を指定したフィールドが直列化されないことを証明するためのプログラムです。このプログラムでは、直列化可能な Point クラスの y フィールドに transient 修飾子を指定しています。そのため、このクラスの y フィールドは直列化されないことを主張しています。
実行結果を見れば、x フィールドに 400、y フィールドに 300 という値を格納している Point オブジェクトを直列化してファイルに保存しています。しかし、これを読み込んだ結果を見ると y フィールドの値がデフォルトの 0 に設定されていることが確認できます。y フィールドは直列化の時に無視されているため、初期値 0 として復元されたのです。