是時候捋一捋 Java 的深淺拷貝了

在開發、刷題、面試中,咱們可能會遇到將一個對象的屬性賦值到另外一個對象的狀況,這種狀況就叫作拷貝。拷貝與Java內存結構息息相關,搞懂Java深淺拷貝是很必要的!程序員

在對象的拷貝中,不少初學者可能搞不清究竟是拷貝了引用仍是拷貝了對象。在拷貝中這裏就分爲引用拷貝、淺拷貝、深拷貝進行講述。面試

引用拷貝

引用拷貝會生成一個新的對象引用地址,可是兩個最終指向依然是同一個對象。如何更好的理解引用拷貝呢?很簡單,就拿咱們人來講,一般有個姓名,可是不一樣場合、人物對咱們的叫法可能不一樣,但咱們很清楚哪些名稱都是屬於"我"的!算法

 

固然,經過一個代碼示例讓你們領略一下(爲了簡便就不寫get、set等方法):json

class Son {
    String name;
    int age;

    public Son(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
public class test {
    public static void main(String[] args) {
        Son s1 = new Son("son1"12);
        Son s2 = s1;
        s1.age = 22;
        System.out.println(s1);
        System.out.println(s2);
        System.out.println("s1的age:" + s1.age);
        System.out.println("s2的age:" + s2.age);
        System.out.println("s1==s2" + (s1 == s2));//相等
    }
}

輸出的結果爲:數組

Son@135fbaa4
Son@135fbaa4
s1的age:22
s2的age:22
true

淺拷貝

如何建立一個對象,將目標對象的內容複製過來而不是直接拷貝引用呢?緩存

這裏先講一下淺拷貝,淺拷貝會建立一個新對象,新對象和原對象自己沒有任何關係,新對象和原對象不等,可是新對象的屬性和老對象相同。具體能夠看以下區別:微信

  • 若是屬性是基本類型(int,double,long,boolean等),拷貝的就是基本類型的值;ide

  • 若是屬性是引用類型,拷貝的就是內存地址(即複製引用但不復制引用的對象) ,所以若是其中一個對象改變了這個地址,就會影響到另外一個對象。函數

若是用一張圖來描述一下淺拷貝,它應該是這樣的:this

 

如何實現淺拷貝呢?也很簡單,就是在須要拷貝的類上實現Cloneable接口並重寫其clone()方法

@Override
protected Object clone() throws CloneNotSupportedException {
  return super.clone();
}

在使用的時候直接調用類的clone()方法便可。具體案例以下:

class Father{
    String name;
    public Father(String name) {
        this.name=name;
    }
    @Override
    public String toString() {
        return "Father{" +
                "name='" + name + '\'' +
                '}';
    }
}
class Son implements Cloneable {
    int age;
    String name;
    Father father;
    public Son(String name,int age) {
        this.age=age;
        this.name = name;
    }
    public Son(String name,int age, Father father) {
        this.age=age;
        this.name = name;
        this.father = father;
    }
    @Override
    public String toString() {
        return "Son{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", father=" + father +
                '}';
    }
    @Override
    protected Son clone() throws CloneNotSupportedException {
        return (Son) super.clone();
    }
}
public class test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Father f=new Father("bigFather");
        Son s1 = new Son("son1",13);
        s1.father=f;
        Son s2 = s1.clone();

        System.out.println(s1);
        System.out.println(s2);
        System.out.println("s1==s2:"+(s1 == s2));//不相等
        System.out.println("s1.name==s2.name:"+(s1.name == s2.name));//相等
        System.out.println();

        //可是他們的Father father 和String name的引用同樣
        s1.age=12;
        s1.father.name="smallFather";//s1.father引用未變
        s1.name="son222";//相似 s1.name=new String("son222") 引用發生變化
        System.out.println("s1.Father==s2.Father:"+(s1.father == s2.father));//相等
        System.out.println("s1.name==s2.name:"+(s1.name == s2.name));//不相等
        System.out.println(s1);
        System.out.println(s2);
    }
}

運行結果爲:

Son{age=13, name='son1', father=Father{name='bigFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}
s1==s2:false
s1.name==s2.name:true//此時相等

s1.Father==s2.Father:true
s1.name==s2.name:false//修改引用後不等
Son{age=12, name='son222', father=Father{name='smallFather'}}
Son{age=13, name='son1', father=Father{name='smallFather'}}

不出意外,這種淺拷貝除了對象自己不一樣之外,各個零部件和關係和拷貝對象都是相同的,就好像雙胞胎同樣,是兩我的,可是其開始的樣貌、各類關係(父母親人)都是相同的。須要注意的是其中name初始==是相等的,是由於初始淺拷貝它們指向一個相同的String,然後s1.name="son222" 則改變引用指向。

 

深拷貝

對於上述的問題雖然拷貝的兩個對象不一樣,但其內部的一些引用仍是相同的,怎麼樣絕對的拷貝這個對象,使這個對象徹底獨立於原對象呢?就使用咱們的深拷貝了。深拷貝:在對引用數據類型進行拷貝的時候,建立了一個新的對象,而且複製其內的成員變量。

 

在具體實現深拷貝上,這裏提供兩個方式,重寫clone()方法和序列法。

重寫clone()方法

若是使用重寫clone()方法實現深拷貝,那麼要將類中全部自定義引用變量的類也去實現Cloneable接口實現clone()方法。對於字符類能夠建立一個新的字符串實現拷貝。

對於上述代碼,Father類實現Cloneable接口並重寫clone()方法。son的clone()方法須要對各個引用都拷貝一遍

//Father clone()方法
@Override
protected Father clone() throws CloneNotSupportedException {
    return (Father) super.clone();
}
//Son clone()方法
@Override
protected Son clone() throws CloneNotSupportedException {
    Son son= (Son) super.clone();//待返回拷貝的對象
    son.name=new String(name);
    son.father=father.clone();
    return son;
}

其餘代碼不變,執行結果以下:

Son{age=13, name='son1', father=Father{name='bigFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}
s1==s2:false
s1.name==s2.name:false

s1.Father==s2.Father:false
s1.name==s2.name:false
Son{age=12, name='son222', father=Father{name='smallFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}

序列化

能夠發現這種方式實現了深拷貝。可是這種狀況有個問題,若是引用數量或者層數太多了怎麼辦呢?

 

不可能去每一個對象挨個寫clone()吧?那怎麼辦呢?藉助序列化啊。

由於序列化後:將二進制字節流內容寫到一個媒介(文本或字節數組),而後是從這個媒介讀取數據,原對象寫入這個媒介後拷貝給clone對象,原對象的修改不會影響clone對象,由於clone對象是從這個媒介讀取。

熟悉對象緩存的知道咱們常常將Java對象緩存到Redis中,而後還可能從Redis中讀取生成Java對象,這就用到序列化和反序列化。通常能夠將Java對象存儲爲字節流或者json串而後反序列化成Java對象。由於序列化會儲存對象的屬性可是不會也沒法存儲對象在內存中地址相關信息。因此在反序列化成Java對象時候會從新建立全部的引用對象。

在具體實現上,自定義的類須要實現Serializable接口。在須要深拷貝的類(Son)中定義一個函數返回該類對象:

protected Son deepClone() throws IOException, ClassNotFoundException {
      Son son=null;
      //在內存中建立一個字節數組緩衝區,全部發送到輸出流的數據保存在該字節數組中
      //默認建立一個大小爲32的緩衝區
      ByteArrayOutputStream byOut=new ByteArrayOutputStream();
      //對象的序列化輸出
      ObjectOutputStream outputStream=new ObjectOutputStream(byOut);//經過字節數組的方式進行傳輸
      outputStream.writeObject(this);  //將當前student對象寫入字節數組中

      //在內存中建立一個字節數組緩衝區,從輸入流讀取的數據保存在該字節數組緩衝區
      ByteArrayInputStream byIn=new ByteArrayInputStream(byOut.toByteArray()); //接收字節數組做爲參數進行建立
      ObjectInputStream inputStream=new ObjectInputStream(byIn);
      son=(Son) inputStream.readObject(); //從字節數組中讀取
      return  son;
}

使用時候調用咱們寫的方法便可,其餘不變,實現的效果爲:

Son{age=13, name='son1', father=Father{name='bigFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}
s1==s2:false
s1.name==s2.name:false

s1.Father==s2.Father:false
s1.name==s2.name:false
Son{age=12, name='son222', father=Father{name='smallFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}

 

固然這是對象的拷貝,對於數組的拷貝將在下一篇進行更細緻的研究!敬請期待!

推薦閱讀

  面試官本想拿一道求素數搞我,但被我優雅的"回擊"了

 16張圖帶你完全搞懂基數排序

「乾貨總結」程序員必知必會的十大排序算法

  花5分鐘看這篇以前,你才發現你不懂RESTful

「五大經常使用算法」一文圖解分治算法和思想

記得關注、我們下次再見!

本文分享自微信公衆號 - bigsai(bigsai)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索