1. 程式人生 > 實用技巧 >Java-物件克隆

Java-物件克隆

1. 在java中,我們通過直接=等號賦值的方法來拷貝,如果是基本資料型別是沒有問題的,

但是如果是引用資料型別,我們拷貝的就是引用,而不是真正的物件,
拷貝引用的後果就是這兩個引用指的還是同一個物件

1. 例如如下程式碼;

class Person {
    private String personName;
    private int age;
    private Integer salary;
    private Car car;
    getset方法,構造器,toString方法
}
class Car{
    private String carName;
    private int price;
    getset方法,構造器,toString方法
}
public class CloneTest{
    public static void main(String[] args) {
        Person p1 = new Person("zxj",21,8000,new Car("bmw",200000));
        Person p2 = p1;

        System.out.println(p1);
        System.out.println(p2);
        
        p2.setAge(10);

        System.out.println(p1);
        System.out.println(p2);
    }
}

2. 列印輸出:我們可以看到我們修改了p2的年齡,但是p1的年齡也隨之修改了,這就是需要克隆的原因,我們需要一個新的Person物件。

Person{personName='zxj', age=21, salary=8000, car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=10, salary=8000, car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=10, salary=8000, car=Car{carName='bmw', price=200000}}

2. 我們先說第一種實現方法,淺拷貝:實現Cloneable介面,並重寫Clone方法

注意:clone方法是Object類的方法,並不是Cloneable介面的方法,實現介面只是為了說明這個類是可克隆的

1. 我們重寫clone方法,並實現介面,clone方法中呼叫super.clone();我們可以看一下父類的clone方法,誒,是native修飾的本地方法,看不了~

class Person implements Cloneable {
    private String personName;
    private int age;
    private Integer salary;
    private Car car;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    getset方法,構造器,toString方法

2. 我們之後在呼叫p1.clone()方法來對p2進行賦值

public class CloneTest{
    public static void main(String[] args) {
        Person p1 = new Person("zxj",21,8000,new Car("bmw",200000));
        Person p2 = null;
        try {
            //會拋異常我們給catch掉
            //clone方法返回是Object型別的,所以需要強轉成Person型別
            p2 = (Person) p1.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println(p1);
        System.out.println(p2);
        
        p2.setAge(10);

        System.out.println(p1);
        System.out.println(p2);
    }
}

3. 這時我們再來執行,我們可以看到這次修改只修改了p2的age的值

Person{personName='zxj', age=21, salary=8000, car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=10, salary=8000, car=Car{carName='bmw', price=200000}}

4. 但是,當我們修改car的price的值的時候,發現也是相同的問題,

p2.getCar().setPrice(1000);
所以我們也需要為Car重複上面的那一套,實現Cloneable介面,並重寫clone方法

3. 我們接下來說第二種方法,深拷貝

對於第一種來說,我們表面上看似是沒有什麼問題,但是我們修改的僅僅是基礎資料型別,如果我們修改引用資料型別,例如List,Car,等等,
我們會發現依舊是修改p2之後,p1也跟著修改

例如:

  • 我們在Person類中新增一個List list欄位,程式碼就不放了
public class CloneTest{
    public static void main(String[] args) {
        Person p1 = new Person("zxj",21,8000,new ArrayList(),new Car("bmw",200000));
        Person p2 = null;
        try {
            p2 = (Person) p1.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println(p1);
        System.out.println(p2);
        
        p2.getList().add("1");
        p2.getCar().setCarName("寶馬");
        p2.getCar().setPrice(100);
        
        System.out.println(p1);
        System.out.println(p2);
        
    }
}
  • 此時我們可以觀察輸出的結果:我們可以看到,對於list我們對p2的list執行的add,但是影響到了p1的list,還有car的carName

Person{personName='zxj', age=21, salary=8000, list=[], car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, list=[], car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, list=[1], car=Car{carName='寶馬', price=100}}
Person{personName='zxj', age=21, salary=8000, list=[1], car=Car{carName='寶馬', price=100}}

這是因為我們在在clone的時候,實際上時對類中的每個屬性都進行一次=等號,賦值,
這就又回到了我們最開始的問題,如果是引用型別,複製的就是引用,而不是物件

1. 我們先說一種解決辦法

我們在Person呼叫clone方法的時候,再呼叫一次Car的clone方法

class Person implements Cloneable {
      。。。省略了其他屬性
    private List list;
    private Car car;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        //首先拿到克隆到的person物件
        Person person = (Person) super.clone();
        //接下來我們對克隆到的person物件的car物件進行復制,呼叫原本car的clone方法進行復制
        person.car = (Car) this.car.clone();
        //然後將我們修改後的克隆person物件返回出去
        return person;
    }
  • 我們再進行測試:我們可以看到,對於car來說,我們的拷貝是沒有問題了,但是這樣子是不是太麻煩了!!!

Person{personName='zxj', age=21, salary=8000, list=[], car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, list=[], car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, list=[1], car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, list=[1], car=Car{carName='寶馬', price=100}}

2. 我們就說另外一種方法,也是正經的方法,通過序列化實現,不知道序列化是啥的可以看我之前的一篇文章

我們可以將物件序列化輸出出去,在讀入進來,這時候讀進來的就是一個新的物件了

1. 首先對於序列化來說,我們的類都必須實現Serializable 介面

class Person implements Serializable {
    private String personName;
    private int age;
    private Integer salary;
    private List list;
    private Car car;
    //依舊getset等等的方法就不寫啦
}

class Car implements Serializable{
    private String carName;
    private int price;
}

public class CloneTest1{
    public static void main(String[] args) {
        Person p1 = new Person("zxj",21,8000,new ArrayList(),new Car("bmw",200000));
        Person p2 = null;
        System.out.println(p1);
        System.out.println(p2);
        
        System.out.println(p1);
        System.out.println(p2);
        
    }
}

2. 因為Clone方法比較通用,我們可以抽取成一個工具類出來

class CopyUtils{
    private CopyUtils() { }

    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj) throws Exception {
        //位元組陣列輸出流在記憶體中建立一個位元組陣列緩衝區,所有傳送到輸出流的資料儲存在該位元組陣列緩衝區中。
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        //使用物件輸出流進行包裝
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        //使用物件流將我們的物件輸出到快取中
        oos.writeObject(obj);
            
        //toByteArray();建立一個新分配的位元組陣列。陣列的大小和當前輸出流的大小,內容是當前輸出流的拷貝。
        //new ByteArrayInputStream(...);位元組陣列輸入流在記憶體中建立一個位元組陣列緩衝區,從輸入流讀取的資料儲存在該位元組陣列緩衝區中,接收位元組陣列作為引數建立。
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);
        //讀取一個物件,並返回出去
        return (T) ois.readObject();
    }
}

3. 接下來就是測試啦:我們看到無論是list還是我們的car修改都是隻修改本身的

public class CloneTest1{
    public static void main(String[] args) {
        Person p1 = new Person("zxj",21,8000,new ArrayList(),new Car("bmw",200000));
        Person p2 = null;
        try {
            p2 = CopyUtils.clone(p1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(p1);
        System.out.println(p2);
        
        p2.getCar().setCarName("寶馬");
        p2.getList().add("1");
        
        System.out.println(p1);
        System.out.println(p2);
        
    }
}

Person{personName='zxj', age=21, salary=8000, list=[], car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, list=[], car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, list=[], car=Car{carName='bmw', price=200000}}
Person{personName='zxj', age=21, salary=8000, list=[1], car=Car{carName='寶馬', price=200000}}