1. 程式人生 > >Java: 淺拷貝和深拷貝的理解和實現方式

Java: 淺拷貝和深拷貝的理解和實現方式

Java中的物件拷貝(Object Copy)指的是將一個物件的所有屬性(成員變數)拷貝到另一個有著相同類型別的物件中去。舉例說明:比如,物件A和物件B都屬於類S,具有屬性a和b。那麼對物件A進行拷貝操作賦值給物件B就是:B.a=A.a;  B.b=A.b;

在程式中拷貝物件是很常見的,主要是為了在新的上下文環境中複用現有物件的部分或全部 資料。

Java中的物件拷貝主要分為:淺拷貝(Shallow Copy)、深拷貝(Deep Copy)。

先介紹一點鋪墊知識:Java中的資料型別分為基本資料型別和引用資料型別。對於這兩種資料型別,在進行賦值操作、用作方法引數或返回值時,會有值傳遞和引用(地址)傳遞的差別。

淺拷貝(Shallow Copy):①對於資料型別是基本資料型別的成員變數,淺拷貝會直接進行值傳遞,也就是將該屬性值複製一份給新的物件。因為是兩份不同的資料,所以對其中一個物件的該成員變數值進行修改,不會影響另一個物件拷貝得到的資料。②對於資料型別是引用資料型別的成員變數,比如說成員變數是某個陣列、某個類的物件等,那麼淺拷貝會進行引用傳遞,也就是隻是將該成員變數的引用值(記憶體地址)複製一份給新的物件。因為實際上兩個物件的該成員變數都指向同一個例項。在這種情況下,在一個物件中修改該成員變數會影響到另一個物件的該成員變數值。

具體模型如圖所示:可以看到基本資料型別的成員變數,對其值建立了新的拷貝。而引用資料型別的成員變數的例項仍然是隻有一份,兩個物件的該成員變數都指向同一個例項。

 

淺拷貝的實現方式主要有三種:

一、通過拷貝構造方法實現淺拷貝:

拷貝構造方法指的是該類的構造方法引數為該類的物件。使用拷貝構造方法可以很好地完成淺拷貝,直接通過一個現有的物件創建出與該物件屬性相同的新的物件。

程式碼參考如下:

/* 拷貝構造方法實現淺拷貝 */
public class CopyConstructor {
    public static void main(String[] args) {
        Age a=new Age(20);
        Person p1=new Person(a,"搖頭耶穌");
        Person p2=new Person(p1);
        System.out.println("p1是"+p1);
        System.out.println("p2是"+p2);
        //修改p1的各屬性值,觀察p2的各屬性值是否跟隨變化
        p1.setName("小傻瓜");
        a.setAge(99);
        System.out.println("修改後的p1是"+p1);
        System.out.println("修改後的p2是"+p2);
    }
}

class Person{
    //兩個屬性值:分別代表值傳遞和引用傳遞
    private Age age;
    private String name;
    public Person(Age age,String name) {
        this.age=age;
        this.name=name;
    }
    //拷貝構造方法
    public Person(Person p) {
        this.name=p.name;
        this.age=p.age;
    }
    
    public void setName(String name) {
        this.name=name;
    }
    
    public String toString() {
        return this.name+" "+this.age;
    }
}

class Age{
    private int age;
    public Age(int age) {
        this.age=age;
    }
    
    public void setAge(int age) {
        this.age=age;
    }
    
    public int getAge() {
        return this.age;
    }
    
    public String toString() {
        return getAge()+"";
    }
}

執行結果為:

p1是搖頭耶穌 20
p2是搖頭耶穌 20
修改後的p1是小傻瓜 99
修改後的p2是搖頭耶穌 99

結果分析:這裡對Person類選擇了兩個具有代表性的屬性值:一個是引用傳遞型別;另一個是字串型別(屬於常量)。

通過拷貝構造方法進行了淺拷貝,各屬性值成功複製。其中,p1值傳遞部分的屬性值發生變化時,p2不會隨之改變;而引用傳遞部分屬性值發生變化時,p2也隨之改變。

要注意:如果在拷貝構造方法中,對引用資料型別變數逐一開闢新的記憶體空間,建立新的物件,也可以實現深拷貝。而對於一般的拷貝構造,則一定是淺拷貝。

二、通過重寫clone()方法進行淺拷貝:

Object類是類結構的根類,其中有一個方法為protected Object clone() throws CloneNotSupportedException,這個方法就是進行的淺拷貝。有了這個淺拷貝模板,我們可以通過呼叫clone()方法來實現物件的淺拷貝。但是需要注意:1、Object類雖然有這個方法,但是這個方法是受保護的(被protected修飾),所以我們無法直接使用。2、使用clone方法的類必須實現Cloneable介面,否則會丟擲異常CloneNotSupportedException。對於這兩點,我們的解決方法是,在要使用clone方法的類中重寫clone()方法,通過super.clone()呼叫Object類中的原clone方法。

參考程式碼如下:對Student類的物件進行拷貝,直接重寫clone()方法,通過呼叫clone方法即可完成淺拷貝。

/* clone方法實現淺拷貝 */
public class ShallowCopy {
    public static void main(String[] args) {
        Age a=new Age(20);
        Student stu1=new Student("搖頭耶穌",a,175);
        
        //通過呼叫重寫後的clone方法進行淺拷貝
        Student stu2=(Student)stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        
        //嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
        stu1.setName("大傻子");
        //改變age這個引用型別的成員變數的值
        a.setAge(99);
        //stu1.setaAge(new Age(99));    使用這種方式修改age屬性值的話,stu2是不會跟著改變的。因為建立了一個新的Age類物件而不是改變原物件的例項值
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 建立年齡類
 */
class Age{
    //年齡類的成員變數(屬性)
    private int age;
    //構造方法
    public Age(int age) {
        this.age=age;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    public String toString() {
        return this.age+"";
    }
}
/*
 * 建立學生類
 */
class Student implements Cloneable{
    //學生類的成員變數(屬性),其中一個屬性為類的物件
    private String name;
    private Age aage;
    private int length;
    //構造方法,其中一個引數為另一個類的物件
    public Student(String name,Age a,int length) {
        this.name=name;
        this.aage=a;
        this.length=length;
    }
    //eclipe中alt+shift+s自動新增所有的set和get方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Age getaAge() {
        return this.aage;
    }
    
    public void setaAge(Age age) {
        this.aage=age;
    }
    
    public int getLength() {
        return this.length;
    }
    
    public void setLength(int length) {
        this.length=length;
    }
    //設定輸出的字串形式
    public String toString() {
        return "姓名是: "+this.getName()+", 年齡為: "+this.getaAge().toString()+", 長度是: "+this.getLength();
    }
    //重寫Object類的clone方法
    public Object clone() {
        Object obj=null;
        //呼叫Object類的clone方法,返回一個Object例項
        try {
            obj= super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

執行結果如下:

姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 大傻子, 年齡為: 99, 長度是: 216
姓名是: 搖頭耶穌, 年齡為: 99, 長度是: 175

其中:Student類的成員變數我有代表性地設定了三種:基本資料型別的成員變數length,引用資料型別的成員變數aage和字串String型別的name.

分析結果可以驗證:

基本資料型別是值傳遞,所以修改值後不會影響另一個物件的該屬性值;

引用資料型別是地址傳遞(引用傳遞),所以修改值後另一個物件的該屬性值會同步被修改。

String型別非常特殊,所以我額外設定了一個字串型別的成員變數來進行說明。首先,String型別屬於引用資料型別,不屬於基本資料型別,但是String型別的資料是存放在常量池中的,也就是無法修改的!也就是說,當我將name屬性從“搖頭耶穌”改為“大傻子"後,並不是修改了這個資料的值,而是把這個資料的引用從指向”搖頭耶穌“這個常量改為了指向”大傻子“這個常量。在這種情況下,另一個物件的name屬性值仍然指向”搖頭耶穌“不會受到影響。

深拷貝:首先介紹物件圖的概念。設想一下,一個類有一個物件,其成員變數中又有一個物件,該物件指向另一個物件,另一個物件又指向另一個物件,直到一個確定的例項。這就形成了物件圖。那麼,對於深拷貝來說,不僅要複製物件的所有基本資料型別的成員變數值,還要為所有引用資料型別的成員變數申請儲存空間,並複製每個引用資料型別成員變數所引用的物件,直到該物件可達的所有物件。也就是說,物件進行深拷貝要對整個物件圖進行拷貝!

簡單地說,深拷貝對引用資料型別的成員變數的物件圖中所有的物件都開闢了記憶體空間;而淺拷貝只是傳遞地址指向,新的物件並沒有對引用資料型別建立記憶體空間。

深拷貝模型如圖所示:可以看到所有的成員變數都進行了複製。

因為建立記憶體空間和拷貝整個物件圖,所以深拷貝相比於淺拷貝速度較慢並且花銷較大。

深拷貝的實現方法主要有兩種:

一、通過重寫clone方法來實現深拷貝

與通過重寫clone方法實現淺拷貝的基本思路一樣,只需要為物件圖的每一層的每一個物件都實現Cloneable介面並重寫clone方法,最後在最頂層的類的重寫的clone方法中呼叫所有的clone方法即可實現深拷貝。簡單的說就是:每一層的每個物件都進行淺拷貝=深拷貝。

參考程式碼如下:

package linearList;
/* 層次呼叫clone方法實現深拷貝 */
public class DeepCopy {
    public static void main(String[] args) {
        Age a=new Age(20);
        Student stu1=new Student("搖頭耶穌",a,175);
        
        //通過呼叫重寫後的clone方法進行淺拷貝
        Student stu2=(Student)stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        System.out.println();
        
        //嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
        stu1.setName("大傻子");
        //改變age這個引用型別的成員變數的值
        a.setAge(99);
        //stu1.setaAge(new Age(99));    使用這種方式修改age屬性值的話,stu2是不會跟著改變的。因為建立了一個新的Age類物件而不是改變原物件的例項值
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 建立年齡類
 */
class Age implements Cloneable{
    //年齡類的成員變數(屬性)
    private int age;
    //構造方法
    public Age(int age) {
        this.age=age;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    public String toString() {
        return this.age+"";
    }
    
    //重寫Object的clone方法
    public Object clone() {
        Object obj=null;
        try {
            obj=super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}
/*
 * 建立學生類
 */
class Student implements Cloneable{
    //學生類的成員變數(屬性),其中一個屬性為類的物件
    private String name;
    private Age aage;
    private int length;
    //構造方法,其中一個引數為另一個類的物件
    public Student(String name,Age a,int length) {
        this.name=name;
        this.aage=a;
        this.length=length;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Age getaAge() {
        return this.aage;
    }
    
    public void setaAge(Age age) {
        this.aage=age;
    }
    
    public int getLength() {
        return this.length;
    }
    
    public void setLength(int length) {
        this.length=length;
    }
    public String toString() {
        return "姓名是: "+this.getName()+", 年齡為: "+this.getaAge().toString()+", 長度是: "+this.getLength();
    }
    //重寫Object類的clone方法
    public Object clone() {
        Object obj=null;
        //呼叫Object類的clone方法——淺拷貝
        try {
            obj= super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        //呼叫Age類的clone方法進行深拷貝
        //先將obj轉化為學生類例項
        Student stu=(Student)obj;
        //學生類例項的Age物件屬性,呼叫其clone方法進行拷貝
        stu.aage=(Age)stu.getaAge().clone();
        return obj;
    }
}

 

姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 大傻子, 年齡為: 99, 長度是: 216
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175

分析結果可以驗證:進行了深拷貝之後,無論是什麼型別的屬性值的修改,都不會影響另一個物件的屬性值。

二、通過物件序列化實現深拷貝

雖然層次呼叫clone方法可以實現深拷貝,但是顯然程式碼量實在太大。特別對於屬性數量比較多、層次比較深的類而言,每個類都要重寫clone方法太過繁瑣。

將物件序列化為位元組序列後,預設會將該物件的整個物件圖進行序列化,再通過反序列即可完美地實現深拷貝。

參考程式碼如下:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/* 通過序列化實現深拷貝 */
public class DeepCopyBySerialization {
    public static void main(String[] args) throws IOException, ClassNotFoundException  {
        Age a=new Age(20);
        Student stu1=new Student("搖頭耶穌",a,175);
        //通過序列化方法實現深拷貝
        ByteArrayOutputStream bos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(bos);
        oos.writeObject(stu1);
        oos.flush();
        ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        Student stu2=(Student)ois.readObject();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        System.out.println();
        //嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
        stu1.setName("大傻子");
        //改變age這個引用型別的成員變數的值
        a.setAge(99);
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 建立年齡類
 */
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;
    }
    
    public String toString() {
        return this.age+"";
    }
}
/*
 * 建立學生類
 */
class Student implements Serializable{
    //學生類的成員變數(屬性),其中一個屬性為類的物件
    private String name;
    private Age aage;
    private int length;
    //構造方法,其中一個引數為另一個類的物件
    public Student(String name,Age a,int length) {
        this.name=name;
        this.aage=a;
        this.length=length;
    }
    //eclipe中alt+shift+s自動新增所有的set和get方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Age getaAge() {
        return this.aage;
    }
    
    public void setaAge(Age age) {
        this.aage=age;
    }
    
    public int getLength() {
        return this.length;
    }
    
    public void setLength(int length) {
        this.length=length;
    }
    //設定輸出的字串形式
    public String toString() {
        return "姓名是: "+this.getName()+", 年齡為: "+this.getaAge().toString()+", 長度是: "+this.getLength();
    }
}

 

執行結果為:

姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 大傻子, 年齡為: 99, 長度是: 216
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175

可以通過很簡潔的程式碼即可完美實現深拷貝。不過要注意的是,如果某個屬性被transient修飾,那麼該屬性就無法被拷貝了。

 以上是淺拷貝的深拷貝的區別和實現方式。

over.