浅いコピーと深いコピー
浅いコピーとは
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が書き換えられていないことがお分かりいただけたと思います。