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.

轉自:https://www.cnblogs.com/shakinghead/p/7651502.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章