Javaでオブジェクトをコピーする際、コピー方法によって挙動が異なるため注意が必要です。
本記事では、Javaにおけるオブジェクトのコピー方法と処理内容の違いについてサンプルコードを掲載しながらご紹介していきます。
目次
Javaオブジェクトを=(イコール)でコピー
まずは作成したオブジェクトを「=(イコール)」を使ってコピーした場合の挙動を確認してみます。
サンプル
public class Main { public static void main(String[] args) { Human human1 = new Human(); human1.name = "山田 太郎"; Human human2 = human1; System.out.println(human1.name); System.out.println(human2.name); human2.name = "佐藤いちろう"; System.out.println(human1.name); System.out.println(human2.name); } } class Human { String name; }
実行結果が下記です。
山田 太郎 山田 太郎 佐藤いちろう 佐藤いちろう
予想していた結果と異なる方もいらっしゃるのではないでしょうか?
変数に格納するようにイコールでコピーを実施すると、human2のnameフィールドだけ値を変更したつもりがhuman1のnamdeフィールドまで変更されてしまっています。
解説
6行目でhuman1のインスタンスを新たに作成したhuman2のインスタンスに格納しており、値がコピーされていることを確認出来ます。
しかしこのコピー方法ではオブジェクトへの参照をコピーしていることとなり、新たに作成したhuman2とhuman1は同じオブジェクトを参照している状態です。
つまりサンプルのようにhuman2のnameフィールドを変更した場合でも、同じオブジェクトを参照しているhuman1のnameフィールドにも影響を与える結果となります。
Javaのcloneメソッドでオブジェクトをコピー
ではどのようにして参照先の異なるオブジェクトとしてコピーするのかというと、Cloneableインターフェースの「clone」メソッドを使用します。
サンプル
public class Main { public static void main(String[] args) { Human human1 = new Human(); human1.name = "山田 太郎"; Human human2 = human1.clone(); System.out.println(human1.name); System.out.println(human2.name); System.out.println("---human2の値だけ変更---"); human2.name = "佐藤いちろう"; System.out.println(human1.name); System.out.println(human2.name); } } class Human implements Cloneable{ String name; @Override public Human clone() { Human human = new Human(); human.name = this.name; return human; } }
実行した結果が下記です。
山田 太郎 山田 太郎 ---human2の値だけ変更--- 山田 太郎 佐藤いちろう
cloneメソッドを利用することで、指定したインスタンスの値限定で変更されていることをご確認頂けます。
解説
17行目の「implements Cloneable」がインターフェースを実装する形でHumanクラスを作成している部分です。
Objectクラスに定義されているcloneメソッドを、20行目に記述したcloneメソッドによりオーバーライドして実装しています。
このようにcloneメソッドを利用したコピーのような方法を「ディープコピー」と呼びます。
newしたJavaオブジェクトへコピー
Javaでは、全く異なるインスタンスオブジェクトとして作成し値をコピーすると、コピー元の値まで書き換えられることはありません。
サンプル
public class Main { public static void main(String[] args) { Human human1 = new Human(); human1.name = "山田 太郎"; Human human2 = new Human(); human2.name = human1.name; System.out.println(human1.name); System.out.println(human2.name); human2.name = "佐藤いちろう"; System.out.println(human1.name); System.out.println(human2.name); } } class Human{ String name; }
実行結果は下記です。
山田 太郎 山田 太郎 山田 太郎 佐藤いちろう
解説
4行目と6行目でそれぞれnew演算子を使用してインスタンス化しているため、全く別のメモリ領域が確保されていることになります。
ただしこの方法の場合、フィールドを1つ1つコピーする必要があるため、大量のフィールドが定義されている場合には大変な作業となってしまいます。
シャローコピーとディープコピーの違い
Javaでのオブジェクトコピー方法を3つご紹介してきましたが、1つ目のイコールを使ったコピー方法を「シャローコピー」、2つ目のcloneメソッドを使ったコピー方法を「ディープコピー」と呼びます。
シャローコピー
シャローコピーは「浅いコピー」とも呼ばれ、実際のデータはコピーされず参照のみがコピーされる言わば見せかけのコピーです。
シャローコピーで作成されたオブジェクトは、実際のデータとしてコピー元と同じメモリの値を参照しているため、オブジェクトの値を変更すればもう片方のオブジェクトの値も自動的に変更されているように見えます。
ディープコピー
ディープコピーは「深いコピー」とも呼ばれ、実際のデータがコピーされた完全に新しいオブジェクトを作成することが出来ます。
メモリ上でも全く別のデータとして扱われるわけですから、オブジェクトの値を変更した場合でも、もう片方のオブジェクトの値が変更されることはありません。
さいごに: Javaでオブジェクトをコピーする際は注意が必要
本記事では、Javaでオブジェクトをコピーする際に利用する3つの方法と処理内容の違いについてご紹介してきました。
シャローコピーとディープコピーの違いをしっかりと把握しておかないと、予期せぬエラーを引き起こす原因となりやすい部分です。
一見シャローコピーでもデータだけをコピーしているようにも見え、テストでも見逃しやすい部分ですので、処理結果の違いに注意して実装するよう意識しておきましょう。
7行目の記述を「human2 = human1;」のように変更してしまうと、最初にご紹介したイコールでの参照コピーとなってしまい、human1とhuman2の参照先が同じになります。