1. 程式人生 > >JAVA clone方法-深複製(深克隆)&淺複製(淺克隆)

JAVA clone方法-深複製(深克隆)&淺複製(淺克隆)

引子

為啥要用clone方法?
最近在專案中發現某開發人員程式碼有問題,然而單元測試也確實不通過,就是對物件的引用失敗造成的
具體如下:
在對某個物件更新儲存資料操作,物件關聯某個檔案需要將物件更新到資料庫後再判斷檔案是否更新(檔案儲存到專門的檔案系統中,物件保持檔案的訪問路徑),如果檔案更新了,那麼就需要上傳物件原來的檔案,因此需要對要更新的物件保留一份副本
然而再程式碼審查的時候,發現小哥哥這樣寫的:
錯誤的物件副本寫法
上圖中錯誤的寫法已經被註釋了,就是使用“另一個”物件oldRecord,用temp賦值給它;
希望用oldRecord儲存住一個temp的副本,然後這是大大的錯誤,後面的使用中就會發現,oldRecord並不能保留住temp的副本,原因就是oldRecord跟temp完全就是相同的一個物件,oldRecord的指向就是temp的引用
在java中,對像temp已經存在記憶體中,它的所有資料位於java堆中,我們所所的temp也是指向資料所在的堆的地址,比如所位於 0x32457D20這個地址下,當把oldRecord = temp;這種賦值,那麼oldRecord的指向地址仍然是0x32457D20,不論是對oldRecord或者temp的修改均是修改了地址 0x32457D20 下面的資料,而 0x32457D20地址是永遠不變的

上面圖中給出了一種修改方法,就是用temp.clone()建立一個物件副本,使用了clone方法就相當於新new了一個物件,顯然新new的物件的地址不可能是0x32457D20(因為這個地址已經被temp或者oldRecord佔用了)

當然了,知道了上面的原理,如果不用clone方法,直接new一個SubSystemDisplay物件出來,然後將temp的每個屬性再分別賦值給新new的物件也行,不過這就顯得有點麻煩了。

下面主要講講java的clone()方法

淺複製與深複製概念

  • 淺複製(淺克隆)
    被複制物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向原來的物件。換言之,淺複製僅僅複製所考慮的物件,而不復制它所引用的物件

  • 深複製(深克隆)
    被複制物件的所有變數都含有與原來的物件相同的值,除去那些引用其他物件的變數。那些引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深複製把要複製的物件所引用的物件都複製了一遍。

Java的clone()方法

  • clone方法將物件複製了一份並返回給呼叫者。一般而言,clone() 方法滿足

    1. 對任何的物件x,都有x.clone() !=x //克隆物件與原物件不是同一個物件
    2. 對任何的物件x,都有x.clone().getClass() == x.getClass() //克隆物件與原物件的型別一樣
    3. 如果物件x的equals()方法定義恰當,那麼x.clone().equals(x)應該成立
  • Java中物件的克隆

    1. 為了獲取物件的一份拷貝,我們可以利用Object類的clone()方法
    2. 在派生類中覆蓋基類的clone()方法,並宣告為public
    3. 在派生類的clone()方法中,呼叫super.clone()
    4. 在派生類中實現Cloneable介面

請看如下程式碼:

public class SubSystemDisplay extends BaseModel implements Cloneable {
    private static final long serialVersionUID = 1L;
    private String sub_code;// 子系統表主鍵sub_code
    private String data_id;// 資料欄位對映表dtjx_transmissioncode主鍵id
    public SubSystemDisplay() {
    }

    public SubSystemDisplay(String sub_code){
        this.sub_code = sub_code;
    }

    public SubSystemDisplay(String sub_code, String data_id){
        this.sub_code = sub_code;
        this.data_id = data_id;
    }

    public String getSub_code() {
        return sub_code;
    }

    public void setSub_code(String sub_code) {
        this.sub_code = sub_code;
    }

    public String getData_id() {
        return data_id;
    }

    public void setData_id(String data_id) {
        this.data_id = data_id;
    }

    @Override
    public SubSystemDisplay clone() {
        SubSystemDisplay o = null;
        try {
            o = (SubSystemDisplay) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return o;
    }

說明:

  1. 為什麼我們在派生類中覆蓋Object的clone()方法時,一定要呼叫super.clone()呢?在執行時刻,Object中的clone()識別出你要複製的是哪一個物件,然後為此物件分配空間,並進行物件的複製,將原始物件的內容一一複製到新物件的儲存空間中。
  2. 繼承自java.lang.Object類的clone()方法是淺複製。這是顯然的,根據引子裡面的java物件底層原理就知道,一個物件存在只要其引用的地址不改變再怎麼引用還是指向這個物件

那應該如何實現深層次的克隆?

深克隆

簡而言之就是物件中對其物件再使用clone()

下面用程式碼說明:

package com.june.clone;

import java.io.Serializable;

/**
 * 抽象類--人
 * @author junehappylove
 *
 */
public abstract class Person implements Serializable{

    private static final long serialVersionUID = 19880316L;

    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

package com.june.clone;

/**
 * 教師類
 * @author junehappylove
 *
 */
public class Teacher extends Person implements Cloneable {

    private static final long serialVersionUID = 19880316L;

    public Teacher(String name, int age){
        super();
        super.setName(name);
        super.setAge(age);
    }

    @Override
    public Object clone(){
        Object o = null;
        try {
            o = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return o;
    }
}

package com.june.clone;

/**
 * 學生類
 * @author junehappylove
 *
 */
public class Student extends Person implements Cloneable {

    private static final long serialVersionUID = 19880316L;

    private Teacher teacher;//學生的老師

    public Student(String name, int age, Teacher teacher){
        super();
        super.setName(name);
        super.setAge(age);
        this.teacher = teacher;
    }

    public Teacher getTeacher() {
        return teacher;
    }

    public void setTeacher(Teacher teacher) {
        this.teacher = teacher;
    }

    @Override
    public Object clone(){
        Student o = null;
        try {
            o = (Student)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }//上面已經可以做到淺層克隆了,深層克隆就需要解決學生的老師物件
        o.setTeacher((Teacher)getTeacher().clone());//這裡設定一下教師的克隆物件即可
        return o;
    }
}

package com.june.clone;

import static org.junit.Assert.*;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * 對學生類的測試
 * @author junehappylove
 *
 */
public class StudentTest {

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test//測試Student/Teacher的深層克隆方法
    public void testClone() {
        Teacher june = new Teacher("june", 30);
        Student xiaoming = new Student("xiaoming", 15, june);
        Student xiaohong = (Student) xiaoming.clone();//小紅是由小明克隆的
        assertFalse(xiaoming.getTeacher().equals(xiaohong.getTeacher()));//此時兩個學生的老師可不是同一個人,雖然名字和年齡都相同,注意了!
        assertFalse(xiaoming.getTeacher() == xiaohong.getTeacher());//相等方法不用 == 判斷,這兩個物件的引用地址明顯是不同的
        // 假設學生小紅的教師變了
        Teacher judy = new Teacher("judy", 28);
        xiaohong.setTeacher(judy);
        // 而小紅使用小明克隆的,那麼小明的老師是否也跟著變化了呢?
        // 如果小明的老師也變了,那麼說明學生Student這個類的clone方法是淺複製的
        // 如果小明的老師仍然是june,那麼說明Student這個類的clone方法是深複製的
        // 這裡參考Student的clone方法,可知學生類是一個深複製
        assertFalse(xiaoming.getTeacher().equals(xiaohong.getTeacher()));
    }

}

上面程式碼中對於深複製(克隆)會發現一個小瑕疵,就是對小紅是從小明克隆而來,他倆什麼都沒做,然而他倆的老師june確是不一樣的,這不合理啊,他們的老師名字相同年齡也相同,他們的老師應該是用一個人才對,除非對小紅的老師重新設定了名稱judy和年齡28,否則june正常情況下就是同一個人,但是測試中發現小紅的老師和小明的老師june確不是同一個人!

啥情況?

注意了 這就是深克隆的一個不足之處,因為深克隆就是把物件的物件重新new了一遍。因此對深克隆,我們總是要“精心”的重寫物件的equals方法。

對於上面的Teacher類,我們可以重寫equals方法,如下:

@Override//對於被深層克隆的物件總是需要重新equals方法
public boolean equals(Object obj) {
    Teacher t = (Teacher)obj;
    return t.getName().equals(this.getName()) && t.getAge() == this.getAge();
}

然而當你重寫了equals方法後,你總是還需要重寫hashCode方法,這就是另外需要討論的了。

利用序列化來做深複製

在Java語言裡深複製一個物件,常常可以先使物件實現Serializable介面,然後把物件(實際上只是物件的一個拷貝)寫到一個流裡,再從流裡讀出來,便可以重建物件。

如下為深複製原始碼

public Object deepClone() {    
    //將物件寫到流裡    
    ByteArrayOutoutStream bo=new ByteArrayOutputStream();    
    ObjectOutputStream oo=new ObjectOutputStream(bo);    
    oo.writeObject(this);    
    //從流裡讀出來    
    ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());    
    ObjectInputStream oi=new ObjectInputStream(bi);    
    return(oi.readObject());    
}   

這樣做的前提是物件以及物件內部所有引用到的物件都是可序列化的,也就是類需要實現java.io.Serializable介面,否則,就需要仔細考察那些不需要序列化的物件,將其設定成transient,從而將之排除在複製過程之外。

總而言之,就是想辦法new從來,而不是引用原物件的地址!