シリアライズ (Serializable)

以下でSerializableインターフェイスの利用方法を検証する。
これは、java.io.Serializableを実装したクラスのインスタンスであれば、 ObjectOutputStreamでインスタンスを簡単にファイルへ出力できて、 ObjectInputStreamで簡単にファイルからインスタンスを復元できる。
この時、インスタンスをStream(1バイトずつ順番に読み書きできるようにする)に変換することから シリアライズと呼ばれます。
後半で、BufferedImageをシリアライズする検討した時の情報を示します。

どれだけ便利かを試す。

以下では、クラス「A」をシリアライズ化して、main でその変数a1を初期設定して、ObjectOutputStreamで保存しています。
その後で、ObjectInputStreamでファイルからインスタンスを変数a2へ復元しています。
(ファイル名は「"TA.bin"」です。)
package application;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class A implements Serializable {//検証用のシリアライズ化したクラス
	int i;
	double d;
	String s;
	int [] buf;
}

public class SerialTest {

	public static void main(String[] args) throws Exception{

		//インスタンスを生成して初期設定
		A a1 = new A();
		a1.i = 123;
		a1.d = 0.987;
		a1.s = "ABC";
		a1.buf = new int []{1,2,3};

		//インスタンスをファイル化
		ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("TA.bin"));
		os.writeObject(a1);
		os.close();

		//インスタンスを変数a2に復元
		ObjectInputStream is = new ObjectInputStream(new FileInputStream("TA.bin"));
		A a2 = (A)is.readObject();
		System.out.println(a2.i);//ここから検証
		System.out.println(a2.d);
		System.out.println(a2.s);
		System.out.println(a2.buf.length);
	}
}
実行結果を示す。(簡単にファイル化できるのが分かりました。)
123
0.987
ABC
3
このファイル内容を試しにバイナリでダンプしてみました。
00000000:  AC ED 00 05 73 72 00 0D 61 70 70 6C 69 63 61 74  |....sr..applicat
00000010:  69 6F 6E 2E 41 16 66 C6 34 6F CF 28 FF 02 00 04  |ion.A.f.4o.(....
00000020:  44 00 01 64 49 00 01 69 5B 00 03 62 75 66 74 00  |D..dI..i[..buft.
00000030:  02 5B 49 4C 00 01 73 74 00 12 4C 6A 61 76 61 2F  |.[IL..st..Ljava/
00000040:  6C 61 6E 67 2F 53 74 72 69 6E 67 3B 78 70 3F EF  |lang/String;xp?.
00000050:  95 81 06 24 DD 2F 00 00 00 7B 75 72 00 02 5B 49  |...$./...{ur..[I
00000060:  4D BA 60 26 76 EA B2 A5 02 00 00 78 70 00 00 00  |M.`&v......xp...
00000070:  03 00 00 00 01 00 00 00 02 00 00 00 03 74 00 03  |.............t..
00000080:  41 42

画像用にBufferedImageのインスタンス変数を追加した実験 (失敗例です。)

前述のクラスに、「BufferedImage img」を追加した。(以下に示す。)
class A implements Serializable {//検証用のシリアライズ化したクラス
	int i;
	double d;
	String s;
	int [] buf;
	BufferedImage img;
}
そして、またmainの最後に、次の確認用表示を追加して実行した。
System.out.println(a2.img);
結果は、次の通り。
123
0.987
ABC
3
null
nullが伝達されて、一見して成功しているように見えるが・・ そこで、実際に、画像を設定して、次のようにmainで設定を追加して検証した。
public class SerialTest {

	public static void main(String[] args) throws Exception{

		//インスタンスを生成して初期設定
		A a1 = new A();
		a1.i = 123;
		a1.d = 0.987;
		a1.s = "ABC";
		a1.buf = new int []{1,2,3};
		a1.img = new BufferedImage(50,50,BufferedImage.TYPE_4BYTE_ABGR);
		Graphics g = a1.img.getGraphics();
		g.setColor(Color.blue);
		g.fillOval(0, 0, 50, 50);

		//インスタンスをファイル化
		ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("TA.bin"));
		os.writeObject(a1);
		os.close();

		//インスタンスを変数a2に復元
		ObjectInputStream is = new ObjectInputStream(new FileInputStream("TA.bin"));
		A a2 = (A)is.readObject();
		System.out.println(a2.i);//ここから検証
		System.out.println(a2.d);
		System.out.println(a2.s);
		System.out.println(a2.buf.length);
		System.out.println(a2.img);
	}
}
}
結果は、次の実行エラーで失敗です。
Exception in thread "main" java.io.NotSerializableException: java.awt.image.BufferedImage
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	・・・・省略
この実行エラーは「Serializableされていないインスタンス変数があるため!」ということでしょう。
シリアライズ対象のインスタンス変数は、基本型と「Serializable」済みのクラスということです。実験で分かるように配列は可能です。
Stringは、以下のように定義されているので、上記実験でできた訳ですが、BufferedImage はSerializableが実装されていないという訳です。
public final class String extends Object implements Serializable, Comparable, CharSequence

上記で動作しない所を、「transient」キーワードで無理やり実行させる。

まず、BufferedImage はSerializableが実装されていないので、「transient」キーワードでシリアライズ対象から外します。
そして、BufferedImageのimgは、 デシリアライズ可能な配列 bufに記憶することで行います。
BufferedImageのimgを配列にして戻すメソッド(getArrayByImage)と、その逆変換するメソッド(getImageByArray)を追加して利用しました。
(最後で、検証のために、デシリアライズしたイメージで、"T.png"の画像を生成しています。)
class A implements Serializable {//検証用のシリアライズ化したクラス
	int i;
	double d;
	String s;
	int [] buf;
	transient BufferedImage img; //直列化不可フィールドの指定

	public static int [] getArrayByImage(BufferedImage img,int w, int h){
		BufferedImage subImg = img.getSubimage(0, 0, w, h);
		WritableRaster raster = subImg.getRaster();
		int size = raster.getNumBands() * w * h;
//		System.out.println("getNumBands:" + size);
		int [] buf = new int[ size ];
		raster.getPixels(0, 0, w, h, buf);
		return buf;
	}

	static BufferedImage getImageByArray(int [] buf,int w, int h){
		BufferedImage img = new BufferedImage(w,h,BufferedImage.TYPE_4BYTE_ABGR);
		WritableRaster raster = img.getRaster();
		raster.setPixels(0, 0, w, h, buf);
		return img;
	}
}

public class SerialTest {
	public static void main(String[] args) throws Exception{
		//インスタンスを生成して初期設定
		A a1 = new A();
		a1.i = 123;
		a1.d = 0.987;
		a1.s = "ABC";
		a1.img = new BufferedImage(w,h,BufferedImage.TYPE_4BYTE_ABGR);
		Graphics g = a1.img.getGraphics();
		g.setColor(Color.blue);
		g.fillOval(0, 0, w, h);
		a1.buf = A.getArrayByImage(a1.img, 50, 50);

		//インスタンスをファイル化
		ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("TA.bin"));
		os.writeObject(a1);
		os.close();

		//インスタンスを変数a2に復元
		ObjectInputStream is = new ObjectInputStream(new FileInputStream("TA.bin"));
		A a2 = (A)is.readObject();
		System.out.println(a2.i);//ここから検証
		System.out.println(a2.d);
		System.out.println(a2.s);
		System.out.println(a2.buf.length);
		a2.img = a2.getImageByArray(a2.buf,50,50);
		ImageIO.write(a2.img, "png", new File("T.png"));//検証でファイル保存
	}
}

利用側で簡単に使えるように変更(独自シリアライズ化)

Serializableをimplementsするクラスは、その宣言通り、 シリアライズと、逆変換するデシリアライズが可能であることを宣言するだけの指定と言えます。
クラスの作成者は、責任を持ってこれらが可能なクラスを設計しなければなりません。
上記のクラス「A」は、シリアライズされたクラスとは言えません。
writeObjectやreadObjectメソッドの実行だけで、インスタンスが保存、読み取りできていないからです。
(imgフィールドを別途に処理する必要があるため)

以下でより正しく、シリアライズ化に変更した例を示します。
その為の方法として、次で示すような、writeObject()とreadObject()メソッドを実装する方法があります。 Serializableインターフェースに定義されているメソッドではないので、オーバーライドするものではないが、それが呼び出される規則になっているようです。
class A implements Serializable {//検証用のシリアライズ化したクラス
	int i;
	double d;
	String s;
	int [] buf;//ここに画像イメージを記憶して使う。

	BufferedImage img; // writeObject、readObjectを作って、defaultWriteObject()やdefaultReadObject()使わない場合、直列化不可フィールドのtransient 指定は、必要ない。

	public static int [] getArrayByImage(BufferedImage img,int w, int h){
		BufferedImage subImg = img.getSubimage(0, 0, w, h);
		WritableRaster raster = subImg.getRaster();
		int size = raster.getNumBands() * w * h;
//		System.out.println("getNumBands:" + size);
		int [] buf = new int[ size ];
		raster.getPixels(0, 0, w, h, buf);
		return buf;
	}

	static BufferedImage getImageByArray(int [] buf,int w, int h){
		BufferedImage img = new BufferedImage(w,h,BufferedImage.TYPE_4BYTE_ABGR);
		WritableRaster raster = img.getRaster();
		raster.setPixels(0, 0, w, h, buf);
		return img;
	}

	private void writeObject(ObjectOutputStream stream) throws IOException {
		stream.putFields();
		stream.writeFields();//親クラスの情報出力
		//上記この2行の代わりに、stream.defaultWriteObject();を使う方法がある。
		// その場合、

		stream.writeInt(this.i);
		stream.writeDouble(this.d);
		stream.writeUTF(this.s);

		this.buf = A.getArrayByImage(this.img, 50,50);
		stream.writeInt(this.buf.length);
		for(int i=0; i < this.buf.length;  i++){
			stream.writeInt(this.buf[i]);
		}
	}

	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
		stream.readFields();
		// 上記代わりに、stream.defaultReadObject();を使う方法がある。

		this.i = stream.readInt();
		this.d = stream.readDouble();
		this.s = stream.readUTF();

		this.buf = new int[stream.readInt()];
		for(int i=0; i < this.buf.length;  i++){
			this.buf[i] = stream.readInt();
		}
		this.img = A.getImageByArray(this.buf,50,50);
	}
}

public class SerialTest {

	public static void main(String[] args) throws Exception{

		//インスタンスを生成して初期設定
		A a1 = new A();
		a1.i = 123;
		a1.d = 0.987;
		a1.s = "ABC";
		a1.img = new BufferedImage(50,50,BufferedImage.TYPE_4BYTE_ABGR);
		Graphics g = a1.img.getGraphics();
		g.setColor(Color.blue);
		g.fillOval(0, 0, 50, 50);

		//インスタンスをファイル化
		ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("TA.bin"));
		os.writeObject(a1);
		os.close();

		//インスタンスを変数a2に復元
		ObjectInputStream is = new ObjectInputStream(new FileInputStream("TA.bin"));
		A a2 = (A)is.readObject();
		System.out.println(a2.i);//ここから検証
		System.out.println(a2.d);
		System.out.println(a2.s);
		System.out.println(a2.buf.length);
		ImageIO.write(a2.img, "png", new File("T.png"));//検証でファイル保存
	}
}

独自シリアライズ化した部品を使う。

前述のクラス定義は、せっかくシリアライズ化を利用しているのに、全てのフィールドを全て読み書きしているので、 冗長的コードになっており、シリアライズ化の恩恵がないと感じられた。
そこで、BufferedImageだけをシリアライズ化した別クラスを定義して、それを利用するように検討した。
以下のMyBufferedImageがシリアライスしたクラスである。
package application;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class MyBufferedImage implements Serializable{

	transient int [] buf; // この変数は、ローカル変数にした方がよい。改良すべき点です。

	transient BufferedImage img; // 直列化不可フィールドの指定

	static int [] getArrayByImage(BufferedImage img){
		int w = img.getWidth();
		int h = img.getHeight();
		int []buf = new int[ w * h ];
		img.getRGB(0, 0, w, h, buf, 0, w);
		return buf;
	}

	static BufferedImage getImageByArray(int [] buf, int width){
		int height = buf.length / width;
		BufferedImage img = new BufferedImage(width,height,BufferedImage.TYPE_4BYTE_ABGR);
		img.setRGB(0, 0, width, height, buf, 0, width);
		return img;
	}

	private void writeObject(ObjectOutputStream stream) throws IOException {
		//stream.putFields();
		//stream.writeFields();//親クラスの情報出力
		//上記この2行の代わりに、
		stream.defaultWriteObject();//を使った。

		int width = this.img.getWidth();
		stream.writeInt(width);

		this.buf = getArrayByImage(this.img);
		stream.writeInt(this.buf.length);
		for(int i=0; i < this.buf.使ったlength;  i++){
			stream.writeInt(this.buf[i]);
		}
	}

	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
		//stream.readFields();
		// 上記代わりに、
		stream.defaultReadObject();//を使った。

		int width = stream.readInt();
		this.buf = new int[stream.readInt()];
		for(int i=0; i < this.buf.length;  i++){
			this.buf[i] = stream.readInt();
		}
		this.img = getImageByArray(this.buf, width);
	}
}
なお、getImageByArrayやgetImageByArrayの作り方は、イメージサイズが変わっても使えるように変更しています。 (WritableRaster を使わない方法に変更しています。)


以上で作成した、MyBufferedImageを使ったシリアライズしたクラスは、次のように簡単になります。
package application;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;

class A implements Serializable {//検証用のシリアライズ化したクラス
	int i;
	double d;
	String s;
	MyBufferedImage img; // シリアライズ化したクラスのインスタンス変数
}

public class SerialTest {
	public static void main(String[] args) throws Exception{

		//インスタンスを生成して初期設定
		A a1 = new A();
		a1.i = 123;
		a1.d = 0.987;
		a1.s = "ABC";
		a1.img = new MyBufferedImage();

		//イメージに描画
		a1.img.img = new BufferedImage(50,50,BufferedImage.TYPE_4BYTE_ABGR);
		Graphics g = a1.img.img.getGraphics();
		g.setColor(Color.blue);
		g.fillOval(0, 0, 50, 50);

		//インスタンスをファイル化
		ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("TA.bin"));
		os.writeObject(a1);
		os.close();

		//インスタンスを変数a2に復元
		ObjectInputStream is = new ObjectInputStream(new FileInputStream("TA.bin"));
		A a2 = (A)is.readObject();
		System.out.println(a2.i);//ここから検証
		System.out.println(a2.d);
		System.out.println(a2.s);
		System.out.println(a2.img.buf.length);
		ImageIO.write(a2.img.img, "png", new File("T.png"));//検証でファイル保存
	}
}
以上で、シリアライズしたインスタンス変数で構成されるクラスのシリアライズ化は、とても簡単になりました。 さて、MyBufferedImageのクラス定義は、streamde.faultWriteObject()やstream.defaultReadObject()を使っているので、 これにより、シリアラスされたインスタンス変数の保存と読み込みは、自動的に行われます。
よって、インスタンス変数や処理手順をうまく検討すると次のように簡単になります。
以前に指定した、bufと width のインスタンス変数 transient 指定を無くして、 この部分の保存と読み込みを、faultWriteObject()とdefaultReadObject()に任せました。
public class MyBufferedImage implements Serializable{

	private static final long serialVersionUID = 2162873589398154617L;

	int [] buf;
	int width;

	transient public BufferedImage img; // 直列化不可フィールドの指定

	static int [] getArrayByImage(BufferedImage img){
		int w = img.getWidth();
		int h = img.getHeight();
		int []buf = new int[ w * h ];
		img.getRGB(0, 0, w, h, buf, 0, w);
		return buf;
	}

	static BufferedImage getImageByArray(int [] buf, int width){
		int height = buf.length / width;
		BufferedImage img = new BufferedImage(width,height,BufferedImage.TYPE_4BYTE_ABGR);
		img.setRGB(0, 0, width, height, buf, 0, width);
		return img;
	}

	private void writeObject(ObjectOutputStream stream) throws IOException {

		this.width = this.img.getWidth();
		this.buf = getArrayByImage(this.img);

		stream.defaultWriteObject();//を使って、this.widthとthis.bufを保存

	}

	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {

		stream.defaultReadObject();//を使って、this.widthとthis.bufを読み取り

		this.img = getImageByArray(this.buf, width);
	}
}
コードは簡単になりました。しかし、int [] buf や int width; のインスタンス変数の情報は、本来imgにある情報なので必要ない記憶情報です。
その意味では、まだ改良が必要なクラスと言えます。

なお、 private static final long serialVersionUID = 2162873589398154617L; の部分は宣言しなくても、動作します。
しかし警告がでるので、追加した。これを使う理由は、別途で説明の予定。

Externalizableを実装する手法のシリアライズ化

シリアライズ化する方法は、上記のようにSerializableインターフェイスを使う以外で、 Externalizableインターフェースを実装する方法があります。
この場合は、writeExternal メソッドとreadExternal メソッドをオーバライドします。
この方法では、親クラスのフィールドも自分で読み書きを実装しなければならないのですが、 シリアライズ化されていいないクラスの継承クラスをシリアライズ化する場合で、使えるでしょう。
その例として、 BufferedImageを継承したMyBufferedImage2にExternalizableインターフェースを実装する例を示します。
package application;

import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class MyBufferedImage2 extends BufferedImage implements Externalizable{

	private static final long serialVersionUID = 538555975269710360L;
	public static int WIDTH =100;
	public static int HEIGHT =100;

	public MyBufferedImage2(){//引数無しのコンストラクタが必要
		super(WIDTH, HEIGHT, TYPE_4BYTE_ABGR);
	}

	public static int [] getArrayByImage(BufferedImage img,int w, int h){
		BufferedImage subImg = img.getSubimage(0, 0, w, h);
		WritableRaster raster = subImg.getRaster();
		int size = raster.getNumBands() * w * h;
//		System.out.println("getNumBands:" + size);
		int [] buf = new int[ size ];
		raster.getPixels(0, 0, w, h, buf);
		return buf;
	}

	public static void setImageByArray(BufferedImage img, int [] buf, int w, int h){
		WritableRaster raster = img.getRaster();
		raster.setPixels(0, 0, w, h, buf);
	}

	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		int w = this.getWidth();
		int h = this.getHeight();
		out.writeInt(w);
		out.writeInt(h);
		int [] buf = getArrayByImage(this,w, h);
		out.writeInt(buf.length);
		for(int i=0; i < buf.length;  i++){
			out.writeInt(buf[i]);
//			System.out.println(i + ":" +buf[i]);
		}
	}

	@Override
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		int w = in.readInt();
		int h = in.readInt();
		int size = in.readInt();
		int [] buf = new int[size];
		for(int i=0; i < buf.length;  i++){
			buf[i] = in.readInt();
			System.out.println(i + ":" +buf[i]);
		}
		setImageByArray(this, buf, w, h);
	}
}
以下で、これを使って検証する例を示す。
package application;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import java.awt.Color;
import java.awt.Graphics;
import java.io.File;
import javax.imageio.ImageIO;

class A implements Serializable {//検証用のシリアライズ化したクラス
	int i;
	double d;
	String s;
	MyBufferedImage2 img; // 直列化不可フィールドの指定
}

public class SerialTest {

	public static void main(String[] args) throws Exception{

		//インスタンスを生成して初期設定
		A a1 = new A();
		a1.i = 123;
		a1.d = 0.987;
		a1.s = "ABC";
		MyBufferedImage2.HEIGHT = 50;
		a1.img = new MyBufferedImage2();

		//イメージに描画
		Graphics g = a1.img.getGraphics();
		g.setColor(Color.yellow);
		g.fillRect(0, 0, 50, 50);
		g.setColor(Color.blue);
		g.fillOval(0, 0, 50, 50);

		//インスタンスをファイル化
		ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("TA.bin"));
		os.writeObject(a1);
		os.close();

		//インスタンスを変数a2に復元
		ObjectInputStream is = new ObjectInputStream(new FileInputStream("TA.bin"));
		A a2 = (A)is.readObject();

		System.out.println(a2.i);//ここから検証
		System.out.println(a2.d);
		System.out.println(a2.s);
//		int []buf = MyBufferedImage.getArrayByImage(a1.img, 100, 100);
//		MyBufferedImage.setImageByArray(a2.img,buf, 100, 100);
		ImageIO.write(a2.img, "png", new File("T.png"));//検証でファイル保存
	}
}