直列化による深いコピー

オブジェクトの直列化とは

複雑な構造を持つオブジェクトの参照をたどって、その末端までの関連するオブジェクトをまとめてバイナリストリームにすることをオブジェクトの直列化といいます。 簡単に言ってしまえば、樹形図のようなややこしいデータをまっすぐな一本の棒に変えてしまうようなものです。 そのストリームをファイルに書き出せば、めでたくデータの保存(永続化)ができてしまうというわけです。

では、そのストリームをパイプ出力というやつに書き出して、もう一度、パイプ入力というやつから読み込むと、一体何が起こるのでしょう。 そうです、われわれが期待する深いコピーがいとも簡単に実現できてしまうのです。

深いコピーの実現

深いコピーの概念についてもう一度図解しておきましょう。詳しくは「浅いコピーと深いコピー」のページを読んでください。

図 1 : 深いコピー

まずはコピーされるデータから見てください。非常にシンプルですが、Serializableインタフェースを実装している点に注意してください。

SerializableData.java
import java.io.*;

public class SerializableData implements Serializable {
    int val;//プリミティブ型
    StringBuffer text;//参照型
}
                    

次のコードはコピーを実行するためのクラスです。いわばコピー機です。「浅いコピーと深いコピー」のページでは、データ自身にコピーの機能を持たせたのですが、ここではコピー機を作って、コピーを実行させます。

DeepCopier.java
import java.io.*;

public class DeepCopier {
    public static Object deepClone(final Serializable o)
        throws IOException,ClassNotFoundException{
        /*オブジェクトoの深いコピーを返すメソッド。*/
        final PipedOutputStream pipeOut=new PipedOutputStream();
        PipedInputStream pipeIn=new PipedInputStream(pipeOut);
        Thread writer=new Thread(){//直列化と書き出しのためのスレッド
            public void run(){
                ObjectOutputStream out=null;
                try{
                    out=new ObjectOutputStream(pipeOut);
                    out.writeObject(o);//直列化とPipedOutputStreamへの書き出し。
                }catch(IOException e){
                }finally{
                    try{
                        out.close();
                    }catch(Exception e){
                    }
                }
            }
        };
        writer.start();//別スレッドで、直列化と書き出しを実行する。
        //PipedInputStreamから読み込み、直列化復元する。
        ObjectInputStream in=new ObjectInputStream(pipeIn);
        return in.readObject();
    }
}
                    

わざわざ別スレッドを作って、そこで直列化と書き出しを行っているのが気になると思います。PipedOutputStreamオブジェクトとPipedInputStreamオブジェクトを同じスレッドで使うことは、そのスレッドをデッドロックされる危険があるので、推奨されていないからです。

次のコードは実行用のクラスです。

DeepCopierTest.java
import java.io.*;

public class DeepCopierTest {
    public static void main(String[] args){
        //コピー元のデータを生成し、初期化
        SerializableData data0=new SerializableData();
        data0.val=1;
        data0.text=new StringBuffer("abcd");
        
        SerializableData data1=null;
        try {
            //データをコピーし、コピー先のデータに値とテキストを設定する。
            data1=(SerializableData)DeepCopier.deepClone(data0);
            data1.val=2;
            data1.text.append("ef");
        }catch(IOException ex){
            System.err.println(ex);
        }catch(ClassNotFoundException ex){
            System.err.println(ex);
        }
        //結果の表示
        System.out.println("data0.val="+data0.val);
        System.out.println("data0.text="+data0.text);
        System.out.println("data1.val="+data1.val);
        System.out.println("data1.text="+data1.text);
    }
}
                    

いざ実行してみましょう。

DeepCopierTestの実行結果
data0.val=1
data0.text=abcd
data1.val=2
data1.text=abcdef
                    

data1.textを書き換えてもdata0.textは影響を受けていませんね。確かに深いコピーができています。

以上オブジェクト直列化による深いコピーの方法を見てきました。この方法には大きなメリットが有ります。 それはSerializableインタフェースを実装するどんなオブジェクトでも、簡単に深いコピーができてしまうことです。 Objectのclone()メソッドをオーバーライドする必要も、フィールドが参照する実体を1個ずつ生成する必要も無いのです。 ただし、あくまでもSerializableインタフェースを実装するクラスだけに使える方法だということに注意してください。 また、浅いコピーで十分な場合や、コピーする必要のないフィールドがある場合などは、この方法を使うとパフォーマンスが落ちる可能性もあるので、どの方法が最適かをよく考えてください。