iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0
Software Development

深入淺出Java 30天系列 第 14

Day 14: 明智地覆寫clone(下)

  • 分享至 

  • xImage
  •  

昨天提到使用繼承而來的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,所以jobcompany欄位一樣不能有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,也更有彈性。


上一篇
Day 13: 明智地覆寫clone(上)
下一篇
Day 15: 總是覆寫toString
系列文
深入淺出Java 30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言