blog

Java上級 - ディープコピーとシャローコピーの違い

シャローコピーはオブジェクトのビット単位のコピーで、元のオブジェクトのプロパティ値を完全にコピーした新しいオブジェクトを作成します。属性が基本型の場合、コピーは基本型の値になります。属性がメモリアドレ...

Jun 21, 2020 · 7 min. read
シェア

浅いコピー

シャローコピーはオブジェクトのビット単位のコピーで、元のオブジェクトのプロパティの値を完全にコピーした新しいオブジェクトを作成します。属性が基本型の場合、コピーは基本型の値になります。属性がメモリアドレスの場合、コピーはメモリアドレスになるので、オブジェクトの一方がそのアドレスの値を変更すると、もう一方のオブジェクトに影響します。つまり、デフォルトのコピーコンストラクタはオブジェクトの浅いコピーしか行いません。

特性

  • ベース・データ型のメンバ・オブジェクトの場合、ベース・データ型は値渡しなので、プロパティ値を新しいオブジェクトに直接代入することになります。ベース・データ型のコピーでは、一方のオブジェクトが値を変更しても、もう一方のオブジェクトには影響しません。
  • 配列やクラスオブジェクトのような参照型の場合、参照型は参照渡しとなるため、シャローコピーは単にメンバ変数にメモリアドレスを代入し、両者が同じメモリ空間を指すようにします。どちらか一方を変更すると、もう一方にも影響します。構造図を以下に示します:

実装オブジェクト・コピーを実装するクラスは、Cloneable インターフェースを実装し、clone() メソッドをオーバーライドする必要があります。

public class Subject { private String name; public Subject(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "[Subject: " + this.hashCode() + ",name:" + name + "]"; } }
public class Student implements Cloneable { private String name; private Integer age; private Subject subject; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Subject getSubject() { return subject; } public void setSubject(Subject subject) { this.subject = subject; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", subject=" + subject + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }

テスト:

@org.junit.jupiter.api.Test
public void run() throws CloneNotSupportedException {
	Subject subject = new Subject("張さん");
 Student student = new Student();
 student.setName("リ・シ");
 student.setAge(20);
 student.setSubject(subject);
 Student student2 = (Student) student.clone();
 student2.setName("ワン・ウー");
 student2.setAge(22);
 Subject subject2 = student2.getSubject();
 subject2.setName("マルコムVI");
 System.out.println(student);
 System.out.println(student2);
}

出力: 説明: 出力から、student.clone() によってコピーされたオブジェクト student2 は student と同じオブジェクトではないことがわかります。studentとstudent2の基本データ型の変更は互いに影響しませんが、参照型のsubjectの変更は影響します。

シャローコピーとオブジェクトコピーの違い

@org.junit.jupiter.api.Test
public void run2() {
 Subject subject = new Subject("張さん");
 Student student = new Student();
 student.setSubject(subject);
 student.setName("リ・シ");
 student.setAge(20);
 Student student2 = student;
 student2.setName("ワン・ウー");
 student2.setAge(18);
 Subject subjectB = student2.getSubject();
 subjectB.setName("lishi");
 System.out.println(student == student2);
 System.out.println("studentA:" + student.toString());
 System.out.println("studentB:" + student2.toString());
}

出力:結果からわかるように、オブジェクトのコピーは新しいオブジェクトを生成せず、2つのオブジェクトのアドレスは同じです。

ディープ・コピー

例えば、student2 の subject を変更したいだけなのに、student の subject も変更されてしまったとします。この場合、ディープコピーが必要となります:

ディープコピーは、参照型のメンバ変数をコピーするときに、参照型のデータメンバ用に別のメモリ空間を作成し、真のコンテンツコピーを実現します。

特徴

  • ベース・データ型のメンバ・オブジェクトの場合、ベース・データ型は値渡しなので、プロパティ値を新しいオブジェクトに直接代入することになります。ベース・データ型のコピーでは、一方のオブジェクトが値を変更しても、もう一方のオブジェクトには影響しません。
  • 配列やクラスオブジェクトのような参照型の場合、ディープコピーは新しいオブジェクト空間を作成し、その内容をコピーします。一方を変更しても、もう一方には影響しません。
  • マルチレイヤーオブジェクトの場合、各オブジェクトはCloneableを実装し、clone()メソッドをオーバーライドする必要があります。
  • ディープコピーはシャローコピーよりも遅く、コストもかかります。

構造図は以下の通りです。

気付く

参照型 Student のメンバ変数 Subject の場合は、Cloneable を実装して clone() メソッドをオーバーライドする必要があります。

public class Subject implements Cloneable { private String name; public Subject(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override protected Object clone() throws CloneNotSupportedException { //Subject 参照型のメンバ・プロパティがある場合は、Studentのように実装する必要がある。 return super.clone(); } @Override public String toString() { return "[Subject: " + this.hashCode() + ",name:" + name + "]"; } }

Studentのclone()メソッドでは、自分自身をコピーして作成された新しいオブジェクトを取得し、新しいオブジェクトの参照型に対してコピー操作を呼び出して、参照型のメンバ変数のディープコピーを実現する必要があります。

public class Student implements Cloneable { private String name; private Integer age; private Subject subject; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Subject getSubject() { return subject; } public void setSubject(Subject subject) { this.subject = subject; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", subject=" + subject + '}'; } @Override protected Object clone() throws CloneNotSupportedException { Student student = (Student) super.clone(); student.subject = (Subject) subject.clone(); return student; } }

テスト:浅いコピーと同じテスト

@org.junit.jupiter.api.Test
public void run() throws CloneNotSupportedException {
	Subject subject = new Subject("張さん");
 Student student = new Student();
 student.setName("リ・シ");
 student.setAge(20);
 student.setSubject(subject);
 Student student2 = (Student) student.clone();
 student2.setName("ワン・ウー");
 student2.setAge(22);
 Subject subject2 = student2.getSubject();
 subject2.setName("マルコムVI");
 System.out.println(student);
 System.out.println(student2);
}

出力:出力からわかるように、ディープコピーの後、それがベースデータ型のメンバ変数であろうと参照型であろうと、その値を変更してもお互いに影響しません。

ディープコピーのためのオブジェクトのシリアライズ

cloneメソッドを階層的に呼び出すことで、ディープコピーを実現できますが、明らかにコード量が多くなりすぎます。特に属性の数が多く階層が深いクラスでは、クラスごとに clone メソッドを書き換えるのは面倒です。

オブジェクトをバイト列としてシリアライズすると、デフォルトではそのオブジェクトのオブジェクトグラフ全体がシリアライズされます。

public class Age implements Serializable {
 private int age;
 public Age(int age) {
 this.age = age;
 }
 public int getAge() {
 return age;
 }
 public void setAge(int age) {
 this.age = age;
 }
 @Override
 public String toString() {
 return "Age{" +
 "age=" + age +
 '}';
 }
}
public class Person implements Serializable { private String name; private Age age; public Person(String name, Age age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Age getAge() { return age; } public void setAge(Age age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }

テスト:

@org.junit.jupiter.api.Test
public void run3() throws IOException, ClassNotFoundException {
 Age a = new Age(20);
 Person person = new Person(" , a);
 ByteArrayOutputStream bos = new ByteArrayOutputStream();
 ObjectOutputStream oos = new ObjectOutputStream(bos);
 oos.writeObject(person);
 oos.flush();
 ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
 Person person2 = (Person) ois.readObject();
 System.out.println(person == person2);
 System.out.println(person);
 System.out.println(person2);
 System.out.println("personのプロパティを変更する");
 person.setName("リ・シ");
 a.setAge(22);
 System.out.println(person);
 System.out.println(person2);
}

出力:出力からわかるように、オブジェクトをシリアライズし、デシリアライズすることによってコピーされたオブジェクトは、新しいオブジェクトを生成しています。

Read next

ArrayList動的展開のソースコード

ArrayListは「自動的に拡張する配列」と考えることができます。では、ArrayListはどのように展開するのでしょうか? まずはArrayListのソースコードを開いてください。私のローカルはJava 1.8バージョンです。最初の質問については、addメソッドを参照してください。

Jun 21, 2020 · 3 min read