JAVA - 淺拷貝與深拷貝

背景知識:拷貝

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

引用拷貝:指創建一個指向對象的引用變量的拷貝

public static void copyReferenceObject(){
    	Car myCar1 = new Car("Benz", 2000);
    	Car myCar2 = myCar1;
        System.out.println(myCar1);
        System.out.println(myCar2);
}

如果我們有一個 Car 對象,而且讓 myCar1變量指向這個變量,這時候當我們做引用拷貝,那麼現在就會有兩個變量myCar1和myCar2,但是對象仍然只存在一個

對象拷貝:創建對象本身的一個副本。因此如果我們再一次服務我們 car 對象,就會創建這個對象本身的一個副本, 同時還會有第二個引用變量指向這個被複製出來的對象。

JAVA的對象拷貝分爲:淺拷貝(Shallow Copy)、深拷貝(Deep Copy)

淺拷貝和深拷貝都是針對一個已有對象的操作

* 淺拷貝Shallow Copy)

對象的淺拷貝會對原始對象進行拷貝,它會創建一個新對象,這個對象有着原始對象屬性值的一份精確拷貝,但不會複製原始對象裏面的對象。“裏面的對象”會在原來的對象和它的副本之間共享

①對於數據類型是基本數據類型的成員變量,淺拷貝會直接進行值傳遞,也就是將該屬性值複製一份給新的對象。因爲是兩份不同的數據,所以對其中一個對象的該成員變量值進行修改,不會影響另一個對象拷貝得到的數據

②對於數據類型是引用數據類型的成員變量,比如說成員變量是某個數組、某個類的對象等,那麼淺拷貝會進行引用變量拷貝,也就是隻是將該成員變量的引用值(引用對應的對象內容的內存地址)複製一份給新的對象。因爲實際上兩個對象的該引用數據類型的成員變量都指向同一個實例。在這種情況下,在一個對象中修改該成員變量引用的對象會影響到另一個對象的該成員變量引用的對象

淺拷貝的實現方式:

通過拷貝構造方法進行淺拷貝

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

public class Test {
    public static void main(String[] args) {
        Person p1=new Person(20,"HAPPY");
        Person p2=new Person(p1);
        p1.setAddr("load");
        System.out.println(p1);
        System.out.println(p2);
    }
}

class Person{
    private int age;
    private String name;
    private Address addr;

	public Person(int age,String name) {
        this.age=age;
        this.name=name;
        this.addr=new Address("street");
    }
    
    //拷貝構造方法,方法體中將所有屬性都進行賦值
    public Person(Person p) {
        this.name=p.name;
        this.age=p.age;
        this.addr=p.addr;
    }    
    
    public void setAddr(String addr) {
		this.addr.setAddress(addr);
	}

    public String toString() {
        return this.name+this.age+this.addr.toString();
    }
}   

class Address{
	private String address;
	
	public Address(String addr) {
		this.address=addr;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	public String toString() {
		return address;
	}	
}

運行結果:

HAPPY20load

HAPPY20load

分析:

Person類裏面包含了一個Address對象。拷貝構造方法會拿到p1對象,然後對其成員變量進行復制。淺拷貝的問題就是兩個對象並非獨立的。如果修改了其中一個Person對象的Address對象,也會影響到另外一個Person對象。

②重寫clone()方法

Object類是類結構的根類,其中有一個方法爲protected Object clone() throws CloneNotSupportedException因爲每個類直接或間接的父類都是Object,因此它們都含有clone()方法,但是因爲該方法是protected,不能在類外進行訪問,所以要想對一個對象進行復制,就需要對clone方法覆蓋

重寫clone()方法的步驟:

* 被複制的類需要實現Clonenable接口(不實現的話在調用clone方法會拋出CloneNotSupportedException異常) 該接口爲標記接口(不含任何方法)

* 重寫clone()方法,訪問修飾符設爲public。方法中通過調用super.clone()方法得到Object類中的原clone方法來複制對象 

public class Test {
    public static void main(String[] args) {
        Age a=new Age(20);
        Student stu1=new Student("HAPPY",a,175);
        
        //通過調用重寫後的clone方法進行淺拷貝
        Student stu2=(Student)stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        
        //嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
        stu1.setName("大傻子");
        a.setAge(99);
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        
        //使用這種方式修改age屬性值的話,stu2是不會跟着改變的。因爲創建了一個新的Age類對象而不是改變原對象的實例值
        stu1.setaAge(new Age(66));           
        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;
    }

    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;
    }
}

運行結果:

姓名是: HAPPY, 年齡爲: 20 , 長度是: 175

姓名是: HAPPY, 年齡爲: 20 , 長度是: 175

姓名是: 大傻子, 年齡爲: 99 , 長度是: 216

姓名是: HAPPY, 年齡爲: 99 , 長度是: 175

姓名是: 大傻子, 年齡爲: 66 , 長度是: 216

姓名是: HAPPY, 年齡爲: 99 , 長度是: 175

* 深拷貝(Deep Copy)

深拷貝不僅要複製對象的所有基本數據類型的成員變量值,還要爲所有引用數據類型的成員變量申請存儲空間,並複製每個引用數據類型成員變量所引用的對象,直到該對象可達的所有對象。也就是說,對象進行深拷貝要對整個對象圖進行拷深拷貝相比於淺拷貝速度較慢並且花銷較大

拷貝的實現方式:

通過拷貝構造方法進行深拷貝

要創建一個真正的深拷貝,不單單對一個類使用了拷貝構造器,同時也會對裏面的對象的類使用拷貝構造器一直這樣拷貝下去,一直覆蓋到一個類所有的內部元素, 最後只剩下原始的類型以及“不可變對象(Immutables)”

public class Test {
    public static void main(String[] args) {
        Person p1=new Person(20,"HAPPY");
        Person p2=new Person(p1);
        p1.setAddr("load");
        System.out.println(p1);
        System.out.println(p2);
    }
}

class Person{
    private int age;
    private String name;
    private Address addr;

	public Person(int age,String name) {
        this.age=age;
        this.name=name;
        this.addr=new Address("street");
    }
    
    //拷貝構造方法,方法體中將所有屬性都進行賦值
    public Person(Person p) {
        this.name=p.name;
        this.age=p.age;
        this.addr=new Address(p.addr);
    }    
    
    public void setAddr(String addr) {
		this.addr.setAddress(addr);
	}

    public String toString() {
        return this.name+this.age+this.addr.toString();
    }
}   

class Address{
	private String address;
	
	public Address(String addr) {
		this.address=addr;
	}
	
	//拷貝構造方法
	public Address(Address addr) {
		this.address=addr.address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	public String toString() {
		return address;
	}	
}

運行結果:

HAPPY20load

HAPPY20street

②重寫clone方法

與通過重寫clone方法實現淺拷貝的基本思路一樣,只需要爲對象圖的每一層的每一個對象都實現Cloneable接口並重寫clone方法,最後在最頂層的類的重寫的clone方法中調用引用變量clone方法即可實現深拷貝。簡單的說就是:每一層的每個對象都進行淺拷貝=深拷貝

public class Test {
    public static void main(String[] args) {
        Age a=new Age(20);
        Student stu1=new Student("HAPPY",a,175);
        
        //通過調用重寫後的clone方法進行深拷貝
        Student stu2=(Student)stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        
        //嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
        stu1.setName("SAD");
        //改變age這個引用類型的成員變量的值
        a.setAge(99);
        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 age;
    private int length;
    //構造方法,其中一個參數爲另一個類的對象
    public Student(String name,Age a,int length) {
        this.name=name;
        this.age=a;
        this.length=length;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Age getaAge() {
        return this.age;
    }
    
    public void setaAge(Age age) {
        this.age=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.age=(Age)stu.getaAge().clone();
        return obj;
    }
}

運行結果:

姓名是: HAPPY, 年齡爲: 20, 長度是: 175

姓名是: HAPPY, 年齡爲: 20, 長度是: 175

姓名是: SAD, 年齡爲: 99, 長度是: 216

姓名是: HAPPY, 年齡爲: 20, 長度是: 175

3通過對象序列化實現深拷貝

雖然層次調用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 Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException  {
        Age a=new Age(20);
        Student stu1=new Student("HAPPY",a,175);
        
        Student stu2=stu1.copy();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());

        //嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
        stu1.setName("SAD");
        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();
    }
    
	//通過序列化方法實現深拷貝
    public Student copy() throws IOException, ClassNotFoundException {
    	ByteArrayOutputStream bos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(bos);
        oos.writeObject(this);
        oos.flush();
        ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        return (Student)ois.readObject();
    }
 }

運行結果:

姓名是: HAPPY, 年齡爲: 20, 長度是: 175

姓名是: HAPPY, 年齡爲: 20, 長度是: 175

姓名是: SAD, 年齡爲: 99, 長度是: 216

姓名是: HAPPY, 年齡爲: 20, 長度是: 175

注:如果某個屬性被transient修飾,那麼該屬性就無法通過序列化被拷貝


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