Java 淺拷貝、深拷貝,你知多少?

這是今天我們在技術羣裏面討論的一個知識點,討論的相當激烈,由於對這一塊使用的比較少,所以對這一塊多少有些盲區。這篇文章總結了所討論的內容,希望這篇文章對你有所幫助。

在 Java 開發中,對象拷貝或者說對象克隆是常有的事,對象克隆最終都離不開直接賦值淺拷貝深拷貝 這三種方式,其中直接賦值應該是我們最常用的一種方式吧,對於淺拷貝和深拷貝可能用的少,所以或多或少存在一些誤區,這篇文章會詳細的介紹這三種對象克隆方式。

前置知識
值類型:Java 的基本數據類型,例如 int、float 
引用類型:自定義類和 Java 包裝類(string、integer)

直接賦值

直接賦值是我們最常用的方式,在我們代碼中的體現是Persona = new Person();Person b = a,是一種簡單明瞭的方式,但是它只是拷貝了對象引用地址而已,並沒有在內存中生成新的對象,我們可以通過下面這個例子來證明這一點

// person 對象
public class Person {
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;
    // 描述
    private String desc;
    ...省略get/set...
 }
 // main 方法
public class PersonApp {
   public static void main(String[] args) {
       // 初始化一個對象
       Person person = new Person("張三",20,"[email protected]","我是張三");
       // 複製對象
       Person person1 = person;
       // 改變 person1 的屬性值
       person1.setName("我不是張三了");
        System.out.println("person對象:"+person);
        System.out.println("person1對象:"+person1);

   }
}

運行上面代碼,你會得到如下結果:

person對象:Person{name='我不是張三了', age=20, email='[email protected]', desc='我是張三'}
person1對象:Person{name='我不是張三了', age=20, email='[email protected]', desc='我是張三'}

我們將 person 對象複製給了 person1 對象,我們對 person1 對象的 name 屬性進行了修改,並未修改 person 對象的name 屬性值,但是我們最後發現 person 對象的 name 屬性也發生了變化,其實不止這一個值,對於其他值也是一樣的,所以這結果證明了我們上面的結論:直接賦值的方式沒有生產新的對象,只是生新增了一個對象引用,直接賦值在 Java 內存中的模型大概是這樣的

淺拷貝

淺拷貝也可以實現對象克隆,從這名字你或許可以知道,這種拷貝一定存在某種缺陷,是的,它就是存在一定的缺陷,先來看看淺拷貝的定義:如果原型對象的成員變量是值類型,將複製一份給克隆對象,也就是說在堆中擁有獨立的空間;如果原型對象的成員變量是引用類型,則將引用對象的地址複製一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的內存地址。換句話說,在淺克隆中,當對象被複制時只複製它本身和其中包含的值類型的成員變量,而引用類型的成員對象並沒有複製。 可能你沒太理解這段話,那麼我們在來看看淺拷貝的通用模型:

淺拷貝通用模型

要實現對象淺拷貝還是比較簡單的,只需要被複制類需要實現 Cloneable 接口,重寫 clone 方法即可,對 person 類進行改造,使其可以支持淺拷貝。

public class Person implements Cloneable {
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;
    // 描述
    private String desc;

	/*
	* 重寫 clone 方法,需要將權限改成 public ,直接調用父類的 clone 方法就好了
	*/
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    ...省略...
}

改造很簡單只需要讓 person 繼承 Cloneable 接口,並且重寫 clone 方法即可,clone 也非常簡單只需要調用 object 的 clone 方法就好,唯一需要注意的地方就是 clone 方法需要用 public 來修飾,在簡單的修改 main 方法

public class PersonApp {
    public static void main(String[] args) throws Exception {
        // 初始化一個對象
        Person person = new Person("張三",20,"[email protected]","我是張三");
        // 複製對象
        Person person1 = (Person) person.clone();
        // 改變 person1 的屬性值
        person1.setName("我是張三的克隆對象");
        // 修改 person age 的值
        person1.setAge(22);
        System.out.println("person對象:"+person);
        System.out.println();
        System.out.println("person1對象:"+person1);

    }
}

重新運行 main 方法,結果如下:

person對象:Person{name='張三', age=20, email='[email protected]', desc='我是張三'}

person1對象:Person{name='我是張三的克隆對象', age=22, email='[email protected]', desc='我是張三'}

看到這個結果,你是否有所質疑呢?說好的引用對象只是拷貝了地址,爲啥修改了 person1 對象的 name 屬性值,person 對象沒有改變?這裏就是一個非常重要的知識點了,,原因在於:String、Integer 等包裝類都是不可變的對象,當需要修改不可變對象的值時,需要在內存中生成一個新的對象來存放新的值,然後將原來的引用指向新的地址,所以在這裏我們修改了 person1 對象的 name 屬性值,person1 對象的 name 字段指向了內存中新的 name 對象,但是我們並沒有改變 person 對象的 name 字段的指向,所以 person 對象的 name 還是指向內存中原來的 name 地址,也就沒有變化

這種引用是一種特列,因爲這些引用具有不可變性,並不具備通用性,所以我們就自定義一個類,來演示淺拷貝,我們定義一個 PersonDesc 類用來存放person 對象中的 desc 字段,,然後在 person 對象中引用 PersonDesc 類,具體代碼如下:

// 新增 PersonDesc 
public class PersonDesc {
    // 描述
    private String desc;

}
public class Person implements Cloneable {
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;
	// 將原來的 string desc 變成了 PersonDesc 對象,這樣 personDesc 就是引用類型
    private PersonDesc personDesc;

    @Override
    public Object clone() throws CloneNotSupportedException {
		return super.clone();
    }
    public void setDesc(String desc) {
        this.personDesc.setDesc(desc);
    }
    public Person(String name, int age, String email, String desc) {
        this.name = name;
        this.age = age;
        this.email = email;
        this.personDesc = new PersonDesc();
        this.personDesc.setDesc(desc);
    }
     ...省略...
}

修改 main 方法

public class PersonApp {
    public static void main(String[] args) throws Exception {
        // 初始化一個對象
        Person person = new Person("平頭哥",20,"[email protected]","我的公衆號是:平頭哥的技術博文");
        // 複製對象
        Person person1 = (Person) person.clone();
        // 改變 person1 的屬性值
        person1.setName("我是平頭哥的克隆對象");
        // 修改 person age 的值
        person1.setAge(22);
        person1.setDesc("我已經關注了平頭哥的技術博文公衆號");
        System.out.println("person對象:"+person);
        System.out.println();
        System.out.println("person1對象:"+person1);
    }
}

運行 main 方法,得到如下結果:

person對象:Person{name='平頭哥', age=20, email='[email protected]', desc='我已經關注了平頭哥的技術博文公衆號'}

person1對象:Person{name='我是平頭哥的克隆對象', age=22, email='[email protected]', desc='我已經關注了平頭哥的技術博文公衆號'}

我們修改 person1 的 desc 字段之後,person 的 desc 也發生了改變,這說明 person 對象和 person1 對象指向是同一個 PersonDesc 對象地址,這也符合淺拷貝引用對象只拷貝引用地址並未創建新對象的定義,到這你應該知道淺拷貝了吧。

深拷貝

深拷貝也是對象克隆的一種方式,相對於淺拷貝,深拷貝是一種完全拷貝,無論是值類型還是引用類型都會完完全全的拷貝一份,在內存中生成一個新的對象,簡單點說就是拷貝對象和被拷貝對象沒有任何關係,互不影響。深拷貝的通用模型如下:

深拷貝通用模型

深拷貝有兩種方式,一種是跟淺拷貝一樣實現 Cloneable 接口,另一種是實現 Serializable 接口,用序列化的方式來實現深拷貝,我們分別用這兩種方式來實現深拷貝

實現 Cloneable 接口方式

實現 Cloneable 接口的方式跟淺拷貝相差不大,我們需要引用對象也實現 Cloneable 接口,具體代碼改造如下:

public class PersonDesc implements Cloneable{

    // 描述
    private String desc;
	...省略...
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Person implements Cloneable {
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;

    private PersonDesc personDesc;

	/**
	* clone 方法不是簡單的調用super的clone 就好,
	*/
    @Override
    public Object clone() throws CloneNotSupportedException {
        Person person = (Person)super.clone();
        // 需要將引用對象也克隆一次
        person.personDesc = (PersonDesc) personDesc.clone();
        return person;
    }
	...省略...
}

main 方法不需要任何改動,我們再次運行 main 方法,得到如下結果:

person對象:Person{name='平頭哥', age=20, email='[email protected]', desc='我的公衆號是:平頭哥的技術博文'}

person1對象:Person{name='我是平頭哥的克隆對象', age=22, email='[email protected]', desc='我已經關注了平頭哥的技術博文公衆號'}

可以看出,修改 person1 的 desc 時對 person 的 desc 已經沒有影響了,說明進行了深拷貝,在內存中重新生成了一個新的對象。

實現 Serializable 接口方式

實現 Serializable 接口方式也可以實現深拷貝,而且這種方式還可以解決多層克隆的問題,多層克隆就是引用類型裏面又有引用類型,層層嵌套下去,用 Cloneable 方式實現還是比較麻煩的,一不小心寫錯了就不能實現深拷貝了,使用 Serializable 序列化的方式就需要所有的對象對實現 Serializable 接口,我們對代碼進行改造,改造成序列化的方式

public class Person implements Serializable {

    private static final long serialVersionUID = 369285298572941L;
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;

    private PersonDesc personDesc;

    public Person clone() {
        Person person = null;
        try { // 將該對象序列化成流,因爲寫在流裏的是對象的一個拷貝,而原對象仍然存在於JVM裏面。所以利用這個特性可以實現對象的深拷貝
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);
            // 將流序列化成對象
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            person = (Person) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return person;
    }

    public void setDesc(String desc) {
        this.personDesc.setDesc(desc);
    }
  ...省略...
}
public class PersonDesc implements Serializable {

    private static final long serialVersionUID = 872390113109L; 
    // 描述
    private String desc;

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

}
public class PersonApp {
    public static void main(String[] args) throws Exception {
        // 初始化一個對象
        Person person = new Person("平頭哥",20,"[email protected]","我的公衆號是:平頭哥的技術博文");
        // 複製對象
        Person person1 = (Person) person.clone();
        // 改變 person1 的屬性值
        person1.setName("我是平頭哥的克隆對象");
        // 修改 person age 的值
        person1.setAge(22);
        person1.setDesc("我已經關注了平頭哥的技術博文公衆號");
        System.out.println("person對象:"+person);
        System.out.println();
        System.out.println("person1對象:"+person1);
    }
}

運行 main 方法,我們可以得到跟 Cloneable 方式一樣的結果,序列化的方式也實現了深拷貝。到此關於 Java 淺拷貝和深拷貝的相關內容就介紹完了,希望你有所收穫。

最後

目前互聯網上很多大佬都有 Java 對象克隆文章,如有雷同,請多多包涵了。原創不易,碼字不易,還希望大家多多支持。若文中有所錯誤之處,還望提出,謝謝。

發佈了82 篇原創文章 · 獲贊 3850 · 訪問量 41萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章