Java深拷貝和淺拷貝

楊充發表於2019-03-25

目錄介紹

  • 01.物件拷貝有哪些
  • 02.理解淺拷貝
    • 2.1 什麼是淺拷貝
    • 2.2 實現淺拷貝案例
  • 03.理解深拷貝
    • 3.1 什麼是深拷貝
    • 3.2 實現深拷貝案例
  • 04.序列化進行拷貝
    • 4.1 序列化屬於深拷貝
    • 4.2 注意要點
    • 4.3 序列化案例
  • 05.延遲拷貝
  • 06.如何選擇拷貝方式
  • 07.陣列的拷貝
    • 7.1 基本資料型別陣列
    • 7.2 引用資料型別陣列
  • 08.集合的拷貝
    • 8.1 集合淺拷貝
    • 8.2 集合深拷貝

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連結地址:github.com/yangchong21…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

01.物件拷貝有哪些

  • 物件拷貝(Object Copy)就是將一個物件的屬性拷貝到另一個有著相同類型別的物件中去。在程式中拷貝物件是很常見的,主要是為了在新的上下文環境中複用物件的部分或全部資料。
  • Java中有三種型別的物件拷貝:淺拷貝(Shallow Copy)、深拷貝(Deep Copy)、延遲拷貝(Lazy Copy)。

02.理解淺拷貝

2.1 什麼是淺拷貝

  • 淺拷貝是按位拷貝物件,它會建立一個新物件,這個物件有著原始物件屬性值的一份精確拷貝。
    • 如果屬性是基本型別,拷貝的就是基本型別的值;如果屬性是記憶體地址(引用型別),拷貝的就是記憶體地址 ,因此如果其中一個物件改變了這個地址,就會影響到另一個物件。
    • image
    • 在上圖中,SourceObject有一個int型別的屬性 "field1"和一個引用型別屬性"refObj"(引用ContainedObject型別的物件)。當對SourceObject做淺拷貝時,建立了CopiedObject,它有一個包含"field1"拷貝值的屬性"field2"以及仍指向refObj本身的引用。由於"field1"是基本型別,所以只是將它的值拷貝給"field2",但是由於"refObj"是一個引用型別, 所以CopiedObject指向"refObj"相同的地址。因此對SourceObject中的"refObj"所做的任何改變都會影響到CopiedObject。

2.2 如何實現淺拷貝

  • 下面來看一看實現淺拷貝的一個例子

    public class Subject {
     
       private String name; 
       public Subject(String s) { 
          name = s; 
       } 
    
       public String getName() { 
          return name; 
       } 
    
       public void setName(String s) { 
          name = s; 
       } 
    }
    複製程式碼
    public class Student implements Cloneable { 
     
       // 物件引用 
       private Subject subj; 
       private String name; 
     
       public Student(String s, String sub) { 
          name = s; 
          subj = new Subject(sub); 
       } 
     
       public Subject getSubj() { 
          return subj; 
       } 
     
       public String getName() { 
          return name; 
       } 
     
       public void setName(String s) { 
          name = s; 
       } 
     
       /** 
        *  重寫clone()方法 
        * @return 
        */ 
       public Object clone() { 
          //淺拷貝 
          try { 
             // 直接呼叫父類的clone()方法
             return super.clone(); 
          } catch (CloneNotSupportedException e) { 
             return null; 
          } 
       } 
    }
    複製程式碼
    private void test1(){
        // 原始物件
        Student stud = new Student("楊充", "瀟湘劍雨");
        System.out.println("原始物件: " + stud.getName() + " - " + stud.getSubj().getName());
    
        // 拷貝物件
        Student clonedStud = (Student) stud.clone();
        System.out.println("拷貝物件: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());
    
        // 原始物件和拷貝物件是否一樣:
        System.out.println("原始物件和拷貝物件是否一樣: " + (stud == clonedStud));
        // 原始物件和拷貝物件的name屬性是否一樣
        System.out.println("原始物件和拷貝物件的name屬性是否一樣: " + (stud.getName() == clonedStud.getName()));
        // 原始物件和拷貝物件的subj屬性是否一樣
        System.out.println("原始物件和拷貝物件的subj屬性是否一樣: " + (stud.getSubj() == clonedStud.getSubj()));
    
        stud.setName("小楊逗比");
        stud.getSubj().setName("瀟湘劍雨大俠");
        System.out.println("更新後的原始物件: " + stud.getName() + " - " + stud.getSubj().getName());
        System.out.println("更新原始物件後的克隆物件: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());
    }
    複製程式碼
  • 輸出結果如下:

    2019-03-23 13:50:57.518 24704-24704/com.ycbjie.other I/System.out: 原始物件: 楊充 - 瀟湘劍雨
    2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 拷貝物件: 楊充 - 瀟湘劍雨
    2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始物件和拷貝物件是否一樣: false
    2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始物件和拷貝物件的name屬性是否一樣: true
    2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始物件和拷貝物件的subj屬性是否一樣: true
    2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 更新後的原始物件: 小楊逗比 - 瀟湘劍雨大俠
    2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 更新原始物件後的克隆物件: 楊充 - 瀟湘劍雨大俠
    複製程式碼
  • 可以得出的結論

    • 在這個例子中,讓要拷貝的類Student實現了Clonable介面並重寫Object類的clone()方法,然後在方法內部呼叫super.clone()方法。從輸出結果中我們可以看到,對原始物件stud的"name"屬性所做的改變並沒有影響到拷貝物件clonedStud,但是對引用物件subj的"name"屬性所做的改變影響到了拷貝物件clonedStud。

03.理解深拷貝

3.1 什麼是深拷貝

  • 深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的記憶體。當物件和它所引用的物件一起拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢並且花銷較大。
    • image
    • 在上圖中,SourceObject有一個int型別的屬性 "field1"和一個引用型別屬性"refObj1"(引用ContainedObject型別的物件)。當對SourceObject做深拷貝時,建立了CopiedObject,它有一個包含"field1"拷貝值的屬性"field2"以及包含"refObj1"拷貝值的引用型別屬性"refObj2" 。因此對SourceObject中的"refObj"所做的任何改變都不會影響到CopiedObject

3.2 實現深拷貝案例

  • 下面是實現深拷貝的一個例子。只是在淺拷貝的例子上做了一點小改動,Subject 和CopyTest 類都沒有變化。
    public class Student implements Cloneable { 
       // 物件引用 
       private Subject subj; 
       private String name; 
     
       public Student(String s, String sub) { 
          name = s; 
          subj = new Subject(sub); 
       } 
     
       public Subject getSubj() { 
          return subj; 
       } 
     
       public String getName() { 
          return name; 
       } 
     
       public void setName(String s) { 
          name = s; 
       } 
     
       /** 
        * 重寫clone()方法 
        * 
        * @return 
        */ 
       public Object clone() { 
          // 深拷貝,建立拷貝類的一個新物件,這樣就和原始物件相互獨立
          Student s = new Student(name, subj.getName()); 
          return s; 
       } 
    }
    複製程式碼
  • 輸出結果如下:
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始物件: 楊充 - 瀟湘劍雨
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 拷貝物件: 楊充 - 瀟湘劍雨
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始物件和拷貝物件是否一樣: false
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始物件和拷貝物件的name屬性是否一樣: true
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始物件和拷貝物件的subj屬性是否一樣: false
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 更新後的原始物件: 小楊逗比 - 瀟湘劍雨大俠
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 更新原始物件後的克隆物件: 楊充 - 瀟湘劍雨
    複製程式碼
  • 得出的結論
    • 很容易發現clone()方法中的一點變化。因為它是深拷貝,所以你需要建立拷貝類的一個物件。因為在Student類中有物件引用,所以需要在Student類中實現Cloneable介面並且重寫clone方法。

04.序列化進行拷貝

4.1 序列化屬於深拷貝

  • 可能你會問,序列化是屬於那種型別拷貝?答案是:通過序列化來實現深拷貝。可以思考一下,為何序列化物件要用深拷貝而不是用淺拷貝呢?

4.2 注意要點

  • 可以序列化是幹什麼的?它將整個物件圖寫入到一個持久化儲存檔案中並且當需要的時候把它讀取回來, 這意味著當你需要把它讀取回來時你需要整個物件圖的一個拷貝。這就是當你深拷貝一個物件時真正需要的東西。請注意,當你通過序列化進行深拷貝時,必須確保物件圖中所有類都是可序列化的。

4.3 序列化案例

  • 看一下下面案例,很簡單,只需要實現Serializable這個介面。Android中還可以實現Parcelable介面。

    public class ColoredCircle implements Serializable { 
     
       private int x; 
       private int y; 
     
       public ColoredCircle(int x, int y) { 
          this.x = x; 
          this.y = y; 
       } 
     
       public int getX() { 
          return x; 
       } 
     
       public void setX(int x) { 
          this.x = x; 
       } 
     
       public int getY() { 
          return y; 
       } 
     
       public void setY(int y) { 
          this.y = y; 
       } 
     
       @Override 
       public String toString() { 
          return "x=" + x + ", y=" + y; 
       } 
    }
    複製程式碼
    private void test3() {
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            // 建立原始的可序列化物件
            DouBi c1 = new DouBi(100, 100);
            System.out.println("原始的物件 = " + c1);
            DouBi c2 = null;
            // 通過序列化實現深拷貝
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bos);
            // 序列化以及傳遞這個物件
            oos.writeObject(c1);
            oos.flush();
            ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
            ois = new ObjectInputStream(bin);
            // 返回新的物件
            c2 = (DouBi) ois.readObject();
            // 校驗內容是否相同
            System.out.println("複製後的物件   = " + c2);
            // 改變原始物件的內容
            c1.setX(200);
            c1.setY(200);
            // 檢視每一個現在的內容
            System.out.println("檢視原始的物件 = " + c1);
            System.out.println("檢視複製的物件 = " + c2);
        } catch (IOException e) {
            System.out.println("Exception in main = " + e);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    複製程式碼
  • 輸出結果如下:

    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始的物件 = x=100, y=100
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 複製後的物件   = x=100, y=100
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 檢視原始的物件 = x=200, y=200
    2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 檢視複製的物件   = x=100, y=100
    複製程式碼
  • 注意:需要做以下幾件事兒:

    • 確保物件圖中的所有類都是可序列化的
    • 建立輸入輸出流
    • 使用這個輸入輸出流來建立物件輸入和物件輸出流
    • 將你想要拷貝的物件傳遞給物件輸出流
    • 從物件輸入流中讀取新的物件並且轉換回你所傳送的物件的類
  • 得出的結論

    • 在這個例子中,建立了一個DouBi物件c1然後將它序列化 (將它寫到ByteArrayOutputStream中). 然後我反序列化這個序列化後的物件並將它儲存到c2中。隨後我修改了原始物件c1。然後結果如你所見,c1不同於c2,對c1所做的任何修改都不會影響c2。
    • 注意,序列化這種方式有其自身的限制和問題:因為無法序列化transient變數, 使用這種方法將無法拷貝transient變數。再就是效能問題。建立一個socket, 序列化一個物件, 通過socket傳輸它, 然後反序列化它,這個過程與呼叫已有物件的方法相比是很慢的。所以在效能上會有天壤之別。如果效能對你的程式碼來說是至關重要的,建議不要使用這種方式。它比通過實現Clonable介面這種方式來進行深拷貝幾乎多花100倍的時間。

05.延遲拷貝

  • 延遲拷貝是淺拷貝和深拷貝的一個組合,實際上很少會使用。這個以前幾乎都沒聽說過,後來看書才知道有這麼一種拷貝!
  • 當最開始拷貝一個物件時,會使用速度較快的淺拷貝,還會使用一個計數器來記錄有多少物件共享這個資料。當程式想要修改原始的物件時,它會決定資料是否被共享(通過檢查計數器)並根據需要進行深拷貝。
  • 延遲拷貝從外面看起來就是深拷貝,但是隻要有可能它就會利用淺拷貝的速度。當原始物件中的引用不經常改變的時候可以使用延遲拷貝。由於存在計數器,效率下降很高,但只是常量級的開銷。而且, 在某些情況下, 迴圈引用會導致一些問題。

06.如何選擇拷貝方式

  • 如果物件的屬性全是基本型別的,那麼可以使用淺拷貝。
  • 如果物件有引用屬性,那就要基於具體的需求來選擇淺拷貝還是深拷貝。
  • 意思是如果物件引用任何時候都不會被改變,那麼沒必要使用深拷貝,只需要使用淺拷貝就行了。如果物件引用經常改變,那麼就要使用深拷貝。沒有一成不變的規則,一切都取決於具體需求。

07.陣列的拷貝

  • 陣列除了預設實現了clone()方法之外,還提供了Arrays.copyOf方法用於拷貝,這兩者都是淺拷貝。

7.1 基本資料型別陣列

  • 如下所示
    public void test4() {
        int[] lNumbers1 = new int[5];
        int[] rNumbers1 = Arrays.copyOf(lNumbers1, lNumbers1.length);
        rNumbers1[0] = 1;
        boolean first = lNumbers1[0] == rNumbers1[0];
        Log.d("小楊逗比", "lNumbers2[0]=" + lNumbers1[0] + ",rNumbers2[0]=" + rNumbers1[0]+"---"+first);
    
        int[] lNumbers3 = new int[5];
        int[] rNumbers3 = lNumbers3.clone();
        rNumbers3[0] = 1;
        boolean second = lNumbers3[0] == rNumbers3[0];
        Log.d("小楊逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"---"+second);
    }
    複製程式碼
  • 列印結果如下所示
    2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/小楊逗比: lNumbers2[0]=0,rNumbers2[0]=1---false
    2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/小楊逗比: lNumbers3[0]=0,rNumbers3[0]=1---false
    複製程式碼

7.2 引用資料型別陣列

  • 如下所示
    public static void test5() {
        People[] lNumbers1 = new People[5];
        lNumbers1[0] = new People();
        People[] rNumbers1 = lNumbers1;
        boolean first = lNumbers1[0].equals(rNumbers1[0]);
        Log.d("小楊逗比", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]+"--"+first);
    
        People[] lNumbers2 = new People[5];
        lNumbers2[0] = new People();
        People[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length);
        boolean second = lNumbers2[0].equals(rNumbers2[0]);
        Log.d("小楊逗比", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]+"--"+second);
    
        People[] lNumbers3 = new People[5];
        lNumbers3[0] = new People();
        People[] rNumbers3 = lNumbers3.clone();
        boolean third = lNumbers3[0].equals(rNumbers3[0]);
        Log.d("小楊逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"--"+third);
    }
    
    public static class People implements Cloneable {
    
        int age;
        Holder holder;
    
        @Override
        protected Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static class Holder {
            int holderValue;
        }
    }
    複製程式碼
  • 列印日誌如下
    2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小楊逗比: lNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18,rNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18--true
    2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小楊逗比: lNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671,rNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671--true
    2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小楊逗比: lNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56,rNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56--true
    複製程式碼

08.集合的拷貝

  • 集合的拷貝也是我們平時經常會遇到的,一般情況下,我們都是用淺拷貝來實現,即通過建構函式或者clone方法。

8.1 集合淺拷貝

  • 建構函式和 clone() 預設都是淺拷貝
    public static void test6() {
        ArrayList<People> lPeoples = new ArrayList<>();
        People people1 = new People();
        lPeoples.add(people1);
        Log.d("小楊逗比", "lPeoples[0]=" + lPeoples.get(0));
        ArrayList<People> rPeoples = (ArrayList<People>) lPeoples.clone();
        Log.d("小楊逗比", "rPeoples[0]=" + rPeoples.get(0));
        boolean b = lPeoples.get(0).equals(rPeoples.get(0));
        Log.d("小楊逗比", "比較兩個物件" + b);
    }
    
    public static class People implements Cloneable {
    
        int age;
        Holder holder;
    
        @Override
        protected Object clone() {
            try {
                People people = (People) super.clone();
                people.holder = (People.Holder) this.holder.clone();
                return people;
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static class Holder implements Cloneable {
    
            int holderValue;
    
            @Override
            protected Object clone() {
                try {
                    return super.clone();
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
    }
    複製程式碼
  • 列印日誌
    2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小楊逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
    2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小楊逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
    2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小楊逗比: 比較兩個物件true
    複製程式碼

8.2 集合深拷貝

  • 在某些特殊情況下,如果需要實現集合的深拷貝,那就要建立一個新的集合,然後通過深拷貝原先集合中的每個元素,將這些元素加入到新的集合當中。
    public static void test7() {
        ArrayList<People> lPeoples = new ArrayList<>();
        People people1 = new People();
        people1.holder = new People.Holder();
        lPeoples.add(people1);
        Log.d("小楊逗比", "lPeoples[0]=" + lPeoples.get(0));
        ArrayList<People> rPeoples = new ArrayList<>();
        for (People people : lPeoples) {
            rPeoples.add((People) people.clone());
        }
        Log.d("小楊逗比", "rPeoples[0]=" + rPeoples.get(0));
        boolean b = lPeoples.get(0).equals(rPeoples.get(0));
        Log.d("小楊逗比", "比較兩個物件" + b);
    }
    
    public static class People implements Cloneable {
    
        int age;
        Holder holder;
    
        @Override
        protected Object clone() {
            try {
                People people = (People) super.clone();
                people.holder = (People.Holder) this.holder.clone();
                return people;
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static class Holder implements Cloneable {
    
            int holderValue;
    
            @Override
            protected Object clone() {
                try {
                    return super.clone();
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
    }
    複製程式碼
  • 列印日誌
    2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小楊逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
    2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小楊逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671
    2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小楊逗比: 比較兩個物件false
    複製程式碼

其他介紹

01.關於部落格彙總連結

02.關於我的部落格

相關文章