浅いコピーと深いコピー
浅いコピーとは
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言語を知ってる人なら、「ああポインタみたいなやつかな。」とピンと来るかもしれません。
言葉だけでは分かりにくいでしょうから、図解してみましょう。まずは勘違いの図です。
正しくは次のようになります。
お分かりいただけたでしょうか。valはプリミティブ型なので、実体がコピーされます。一方textは参照型なので、参照がコピーされるだけなのです。
以上のように参照型フィールドの実体はそのままで、参照だけがコピーされることを浅いコピーといいます。
深いコピーとは
ここまでくれば深いコピーの意味もおのずとお分かりでしょう。そうです、参照先の実体も含めて別の場所にコピーすることを深いコピーというです。
念のため図解しておきましょう。
これから、この深いコピーを実現しようと思います。
まず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が書き換えられていないことがお分かりいただけたと思います。