浅いコピーと深いコピー

浅いコピーとは

Objectのclone()メソッドをオーバーライドして、Cloneableインタフェースを実装することで、コピー(クローン)可能なクラスを作ることができます。
ただし、これには浅いコピー深いコピーというものがあり、初心者泣かせの曲者です。まずは浅いコピーの実例から見ていきましょう。
次のコードはコピー可能なデータを表します。

ShallowCloneableData.java
public class ShallowCloneableData implements Cloneable {
    int val;//プリミティブ型
    StringBuffer text;//参照型
    
    public Object clone(){
        try{
            return super.clone();
        }catch(CloneNotSupportedException ex){
            System.err.println(ex);
            return null;
        }
    }
}
                    

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

ShallowCopyTest.java
public class ShallowCopyTest {
    public static void main(String[] args){
        //コピー元のデータを生成し、初期化
        ShallowCloneableData data0=new ShallowCloneableData();
        data0.val=1;
        data0.text=new StringBuffer("abcd");
        
        //データをコピーし、コピー先のデータに値とテキストを設定する。
        ShallowCloneableData data1=(ShallowCloneableData)data0.clone();
        data1.val=2;
        data1.text.append("ef");
        
        //結果の表示
        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);
    }
}
                    

実行する前に、結果がどうなるか予想してみると、面白いかもしれませんよ。

では実行してみましょう。

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

さあ、どうでしょう。予想した通りになりましたか。
valの方は問題有りませんね。しかしtextの方が妙なことになってます。data1.textだけに"ef"を追加したはずなのに、なぜかdata0.textの方まで書き換えられてしまいました。
data0の内容をまるごとコピーして別の場所にdata1を作ったように思えるかもしれませんが、本当はデータの内容すべてがコピーされたわけではないのです。
valはint型というプリミティブ型であり、textはStringBuffer型という参照型である点に注意してください。
valの方は、本当に実体が別の場所にコピーされたので、data0.valとdata1.valは別物です。一方textの方は実体がコピーされたのではなく、参照がコピーされただけなので、data0.textの実体とdata1.textの実体は同じ物なのです。参照とは即ち、実体の置かれているメモリ上の場所を示すアドレスのことです。
実体が同じであるからには、data1.textに"ef"を追加するということは即ち、data0.textに"ef"を追加してることになるわけです。
このことはC言語を知ってる人なら、「ああポインタみたいなやつかな。」とピンと来るかもしれません。
言葉だけでは分かりにくいでしょうから、図解してみましょう。まずは勘違いの図です。

図 1 : 浅いコピーをこういうものだと勘違いすることがある

正しくは次のようになります。

図 2 : 浅いコピーを正しく理解する

お分かりいただけたでしょうか。valはプリミティブ型なので、実体がコピーされます。一方textは参照型なので、参照がコピーされるだけなのです。
以上のように参照型フィールドの実体はそのままで、参照だけがコピーされることを浅いコピーといいます。

深いコピーとは

ここまでくれば深いコピーの意味もおのずとお分かりでしょう。そうです、参照先の実体も含めて別の場所にコピーすることを深いコピーというです。
念のため図解しておきましょう。

図 3 : 深いコピー

これから、この深いコピーを実現しようと思います。
まずDeepCloneableDataのコードを見てください。

DeepCloneableData.java
public class DeepCloneableData implements Cloneable {
    int val;//プリミティブ型
    StringBuffer text;//参照型
        
    public Object clone(){
        try{
            DeepCloneableData c=(DeepCloneableData)super.clone();
            c.text=new StringBuffer(text);//textの実体をコピー
            return c;
        }catch(CloneNotSupportedException ex){
            System.err.println(ex);
            return null;
        }
    }
}
                    

ShallowCloneableDataとの違いはtryブロックの中だけです。
親クラスのclone()を呼び出した後、StringBufferのコンストラクタを使って、新しいインスタンスを作り、textに代入しています。これにより、textが他の場所にコピーされた実体を参照するようになります。

次は実行用のクラスです。こちらはShallowCopyTestと実質的に変わっていません。ShallowCloneableDataをDeepCloneableDataに変更しただけです。

DeepCopyTest.java
public class DeepCopyTest {
    public static void main(String[] args){
        //コピー元のデータを生成し、初期化
        DeepCloneableData data0=new DeepCloneableData();
        data0.val=1;
        data0.text=new StringBuffer("abcd");
        
        //データをコピーし、コピー先のデータに値とテキストを設定する。
        DeepCloneableData data1=(DeepCloneableData)data0.clone();
        data1.val=2;
        data1.text.append("ef");
        
        //結果の表示
        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);
    }
}
                    

では実行結果を見てみましょう。

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

今度はdata0.textが書き換えられていないことがお分かりいただけたと思います。