昨天提到使用繼承而來的clone
,所衍生的一些問題。要解決昨天遇到的問題,最好的方法就是覆寫clone()
,因為可以在clone()
裡面去複製array或list,這樣就可以避免兩個物件會指向同一個array或list的問題。從下面的範例可以發現,兩個物件用System.identityHashCode
取得的值不一樣了,代表兩個物件真的指向不同的array。
import java.util.ArrayList;
import java.util.List;
class PersonForClone implements Cloneable {
private String name;
private int age;
private String[] jobs;
public PersonForClone(String name, int age , String[] jobs) {
this.name = name;
this.age = age;
this.jobs = jobs;
}
@Override public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age + '\'' +
", jobs=" + jobs +
'}';
}
@Override protected Object clone() throws CloneNotSupportedException {
PersonForClone clone = (PersonForClone) super.clone();
clone.jobs = jobs.clone(); // Create a new list and copy the items, not just reference the original list.
return clone;
}
public static void main(String[] args) {
String[] jobs = {"Developer", "Designer", "PM"};
System.out.println("p1 jobs reference: " + Integer.toHexString(System.identityHashCode(jobs)));
PersonForClone p1 = new PersonForClone("Lucky", 10, jobs);
try {
PersonForClone p2 = (PersonForClone) p1.clone();
System.out.println("p2: " + p2);
System.out.println("p2 jobs reference: " + Integer.toHexString(System.identityHashCode(p2.jobs)));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
但在覆寫的時候必須注意,由於需要把新的array指定給clone出來的物件,如果類別的array的欄位是final
,因為clone()
是在新的物件被實體化之後才開始複製欄位的值,但實體化之後final
就無法被更動,會出現cannot assign a value to final variable jobs
的錯誤訊息,所以必須移除欄位的final
屬性。
另外,如果array裡面放置的物件又有包含其他物件或collection,就不能只用array的clone()
去複製,雖然array的reference不一樣,但是array的每個item的reference也不一樣,但是每個item裡面的物件或collection還是指到同一個reference,這樣item裡面的物件或collection在任何一個地方有變動時,都會影響另外一個item的結果。
class Company {
private String name;
public Company(String name) {
this.name = name;
}
}
class Job {
private String title;
private String description;
private Company company;
public Job(String title, String description, Company company) {
this.title = title;
this.description = description;
this.company = company;
}
public Company getCompany() {
return company;
}
}
class PersonForClone implements Cloneable {
private String name;
private int age;
private Job[] jobs;
public PersonForClone(String name, int age , Job[] jobs) {
this.name = name;
this.age = age;
this.jobs = jobs;
}
@Override public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age + '\'' +
", jobs=" + jobs +
'}';
}
@Override protected Object clone() throws CloneNotSupportedException {
PersonForClone clone = (PersonForClone) super.clone();
clone.jobs = jobs.clone(); // Create a new list and copy the items, not just reference the original list.
return clone;
}
public static void main(String[] args) {
Company company1 = new Company("Banana");
Job job1 = new Job("Developer", "Backend", company1);
Company company2 = new Company("Grava");
Job job2 = new Job("Designer", "UX/UI", company2);
Job[] jobs = {job1, job2};
System.out.println("p1 jobs reference: " + Integer.toHexString(System.identityHashCode(jobs)));
System.out.println("p1 job1 reference: " + Integer.toHexString(System.identityHashCode(jobs[0])));
System.out.println("p1 job1 company reference: " + Integer.toHexString(System.identityHashCode(jobs[0].getCompany())));
PersonForClone p1 = new PersonForClone("Lucky", 10, jobs);
try {
PersonForClone p2 = (PersonForClone) p1.clone();
System.out.println("p2: " + p2);
System.out.println("p2 jobs reference: " + Integer.toHexString(System.identityHashCode(p2.jobs)));
System.out.println("p2 job1 reference: " + Integer.toHexString(System.identityHashCode(p2.jobs[0])));
System.out.println("p2 job1 company reference: " + Integer.toHexString(System.identityHashCode(p2.jobs[0].getCompany())));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
從上面的範例可以看到,jobs
的reference不一樣,job1
的reference也不一樣,但是job1
裡面的company
還是指到同一個company
物件。
這時候就需要更進一步的deep copy,用迴圈或recursive的方式去把每一個物件進行clone,就像下面範例,針對jobs
的每個item都另外做複製,然後跟之前遇到的問題一樣,由於clone()
並不是使用constructor,所以job
的company
欄位一樣不能有final
屬性。
class Company {
private String name;
public Company(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Job implements Cloneable {
private String title;
private String description;
private Company company;
public Job(String title, String description, Company company) {
this.title = title;
this.description = description;
this.company = company;
}
public Company getCompany() {
return company;
}
@Override protected Object clone() throws CloneNotSupportedException {
Job clone = (Job) super.clone();
clone.company = new Company(company.getName()); // Deep copy for Company object
return clone;
}
}
class PersonForClone implements Cloneable {
private String name;
private int age;
private Job[] jobs;
public PersonForClone(String name, int age , Job[] jobs) {
this.name = name;
this.age = age;
this.jobs = jobs;
}
@Override public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age + '\'' +
", jobs=" + jobs +
'}';
}
@Override protected Object clone() throws CloneNotSupportedException {
PersonForClone clone = (PersonForClone) super.clone();
clone.jobs = new Job[jobs.length];
for (int i = 0; i < jobs.length; i++) {
clone.jobs[i] = (Job) jobs[i].clone();
}
return clone;
}
public static void main(String[] args) {
Company company1 = new Company("Banana");
Job job1 = new Job("Developer", "Backend", company1);
Company company2 = new Company("Grava");
Job job2 = new Job("Designer", "UX/UI", company2);
Job[] jobs = {job1, job2};
System.out.println("p1 jobs reference: " + Integer.toHexString(System.identityHashCode(jobs)));
System.out.println("p1 job1 reference: " + Integer.toHexString(System.identityHashCode(jobs[0])));
System.out.println("p1 job1 company reference: " + Integer.toHexString(System.identityHashCode(jobs[0].getCompany())));
PersonForClone p1 = new PersonForClone("Lucky", 10, jobs);
try {
PersonForClone p2 = (PersonForClone) p1.clone();
System.out.println("p2 jobs reference: " + Integer.toHexString(System.identityHashCode(p2.jobs)));
System.out.println("p2 job1 reference: " + Integer.toHexString(System.identityHashCode(p2.jobs[0])));
System.out.println("p2 job1 company reference: " + Integer.toHexString(System.identityHashCode(p2.jobs[0].getCompany())));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
可以看到company
做了deep copy之後,兩個物件指的reference就不一樣了。
最後,雖然前面介紹很多clone()
,但如果真的需要複製一個物件,建議還是提供一個專門複製物件的consturctor或factory,像是:
public Person(Person person);
或
public static Person newInstance(Person person);
這樣的好處是不需要做強制轉型,透過consturctor也不需要調整final
屬性,而且如果想要更換型別,例如從HashSet
改成TreeSet
,也更有彈性。