淺談Java中的淺拷貝和深拷貝

寫在前面的


前幾天在複習這塊內容的時候看到了幾篇不錯的博客,文章讀起來通俗易懂,今天打算把它們整合一下記錄下來。(注:原文連接在此文章的底部)java

假如你想複製一個簡單變量,很簡單:bash

int apples = 5;
int pears = apples; 
複製代碼

不只僅是int類型,其它七種基本數據類型(byte,short,long,double,float,char,boolean)一樣適用於該類狀況。app

可是若是你想複製一個對象,那狀況就稍有些複雜了,可能你會這麼寫:socket

class Student { 
    private int number; 
 
    public int getNumber() { 
        return number; 
    } 
 
    public void setNumber(int number) { 
        this.number = number; 
    } 
    
} 
public class Test { 
     
    public static void main(String args[]) { 
         
        Student stu1 = new Student(); 
        stu1.setNumber(12345); 
        Student stu2 = stu1; 
         
        System.out.println("學生1:" + stu1.getNumber()); 
        System.out.println("學生2:" + stu2.getNumber()); 
    } 
} 
複製代碼

打印結果:ide

學生1:12345 
學生2:12345 
複製代碼

這裏咱們定義了一個Student類,該類只有一個number字段。而後咱們new了一個Student實例,而後將該值賦值給stu2實例(Student stu2 = stu1;)。再看打印結果,做爲一個新手,感受對象複製不過如此,難道真的就是這樣的嗎?接下來咱們試着改變stu2實例的number字段,再打印結果看看:函數

stu2.setNumber(54321); 
 
System.out.println("學生1:" + stu1.getNumber()); 
System.out.println("學生2:" + stu2.getNumber()); 
複製代碼

打印結果:佈局

學生1:54321 
學生2:54321 
複製代碼

這就奇怪了,爲何改變stu2number值,stu1number值也發生了改變呢?post

緣由出在stu2 = stu1這一句,該句的做用是將stu1的引用賦值給stu2,這樣stu1stu2指向內存堆中的同一個對象。如圖: 性能

那麼,怎麼能達到複製一個對象呢,對於「複製對象」,咱們應該有什麼要特別注意的地方呢?接下來就引出今天討論的主角: 對象拷貝。

如今纔是概述


  對象拷貝(Object Copy)就是將一個對象的屬性拷貝到另外一個有着相同類類型的對象中去。在程序中拷貝對象是很常見的,主要是爲了在新的上下文環境中複用對象的部分或所有數據。Java中有三種類型的對象拷貝:淺拷貝(Shallow Copy)、深拷貝(Deep Copy)、延遲拷貝(Lazy Copy)。ui

1. 淺拷貝

  • 什麼是淺拷貝?

  淺拷貝是按位拷貝對象,它會建立一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。若是屬性是基本類型,拷貝的就是基本類型的值;若是屬性是內存地址(引用類型),拷貝的就是內存地址 ,所以若是其中一個對象改變了這個地址,就會影響到另外一個對象。

在上圖中, SourceObject有一個 int類型的屬性 " field1"和一個引用類型屬性" refObj"(引用 ContainedObject類型的對象)。當對 SourceObject作淺拷貝時,建立了 CopiedObject,它有一個包含" field1"拷貝值的屬性" field2"以及仍指向 refObj自己的引用。因爲" field1"是基本類型,因此只是將它的值拷貝給" field2",可是因爲" refObj"是一個引用類型, 因此 CopiedObject指向" refObj"相同的地址。所以對 SourceObject中的" refObj"所作的任何改變都會影響到 CopiedObject

  • 如何實現淺拷貝

是否記得萬類之王Object。它有9個方法(getClass(), hashCode(), equals(), clone(), toString(), notify(), notifyAll(), wait(), finalize()),其中一個爲clone()方法。 該方法的簽名是:

protected native Object clone() throws CloneNotSupportedException;
複製代碼

由於每一個類直接或間接的父類都是Object,所以它們都含有clone()方法。 要想對一個對象進行復制,就須要對clone()方法覆蓋。

關於clone

clone顧名思義就是複製, 在Java語言中, clone方法被對象調用,因此會複製對象。所謂的複製對象,首先要分配一個和源對象一樣大小的空間,在這個空間中建立一個新的對象。那麼在java語言中,有幾種方式能夠建立對象呢?

1. 使用new操做符建立一個對象

2. 使用clone方法複製一個對象

  那麼這兩種方式有什麼相同和不一樣呢? new操做符的本意是分配內存。程序執行到new操做符時, 首先去看new操做符後面的類型,由於知道了類型,才能知道要分配多大的內存空間。分配完內存以後,再調用構造函數,填充對象的各個域,這一步叫作對象的初始化,構造方法返回後,一個對象建立完畢,能夠把他的引用(地址)發佈到外部,在外部就可使用這個引用操縱這個對象。(具體細節可參考個人另外一篇文章《JVM之對象的建立、內存佈局、訪問總結》

  而clone在第一步是和new類似的, 都是分配內存,調用clone方法時,分配的內存和源對象(即調用clone方法的對象)相同,而後再使用原對象中對應的各個域,填充新對象的域, 填充完成以後,clone方法返回,一個新的相同的對象被建立,一樣能夠把這個新對象的引用發佈到外部。

下面是淺拷貝的一個例子

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; 
      } 
   } 
}
複製代碼
public class CopyTest {

    public static void main(String[] args) {
        // 原始對象
        Student stud = new Student("John", "Algebra");
        System.out.println("Original Object: " + stud.getName() + " - " + stud.getSubj().getName());

        // 拷貝對象
        Student clonedStud = (Student) stud.clone();
        System.out.println("Cloned Object: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());

        // 原始對象和拷貝對象是否同樣:
        System.out.println("Is Original Object the same with Cloned Object: " + (stud == clonedStud));
        // 原始對象和拷貝對象的name屬性是否同樣
        System.out.println("Is Original Object's field name the same with Cloned Object: " + (stud.getName() == clonedStud.getName()));
        // 原始對象和拷貝對象的subj屬性是否同樣
        System.out.println("Is Original Object's field subj the same with Cloned Object: " + (stud.getSubj() == clonedStud.getSubj()));

        stud.setName("Dan");
        stud.getSubj().setName("Physics");

        System.out.println("Original Object after it is updated: " + stud.getName() + " - " + stud.getSubj().getName());
        System.out.println("Cloned Object after updating original object: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());
    }
}

複製代碼

​ 輸出結果以下:

Original Object: John - Algebra
Cloned Object: John - Algebra
Is Original Object the same with Cloned Object: false
Is Original Object's field name the same with Cloned Object: true Is Original Object's field subj the same with Cloned Object: true
Original Object after it is updated: Dan - Physics
Cloned Object after updating original object: John - Physics
複製代碼

在這個例子中,我讓要拷貝的類Student實現了Clonable接口並重寫Object類的clone()方法,而後在方法內部調用super.clone()方法。從輸出結果中咱們能夠看到,對原始對象stud"name"屬性所作的改變並無影響到拷貝對象clonedStud,可是對引用對象subj"name"屬性所作的改變影響到了拷貝對象clonedStud

2. 深拷貝

  • 什麼是深拷貝

  深拷貝會拷貝全部的屬性,並拷貝屬性指向的動態分配的內存。當對象和它所引用的對象一塊兒拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢而且花銷較大。

在上圖中, SourceObject有一個int類型的屬性 " field1"和一個引用類型屬性" refObj1"(引用ContainedObject類型的對象)。當對 SourceObject作深拷貝時,建立了 CopiedObject,它有一個包含" field1"拷貝值的屬性" field2"以及包含" refObj1"拷貝值的引用類型屬性" refObj2" 。所以對 SourceObject中的" refObj"所作的任何改變都不會影響到 CopiedObject

  • 如何實現深拷貝

下面是實現深拷貝的一個例子。只是在淺拷貝的例子上作了一點小改動,SubjectCopyTest 類都沒有變化。

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; 
   } 
}
複製代碼

輸出結果以下:

Original Object: John - Algebra
Cloned Object: John - Algebra
Is Original Object the same with Cloned Object: false
Is Original Object's field name the same with Cloned Object: true Is Original Object's field subj the same with Cloned Object: false
Original Object after it is updated: Dan - Physics
Cloned Object after updating original object: John - Algebra
複製代碼

很容易發現clone()方法中的一點變化。由於它是深拷貝,因此你須要建立拷貝類的一個對象。由於在Student類中有對象引用,因此須要在Student類中實現Cloneable接口而且重寫clone方法。

3. 經過序列化實現深拷貝

也能夠經過序列化來實現深拷貝。序列化是幹什麼的?它將整個對象圖寫入到一個持久化存儲文件中而且當須要的時候把它讀取回來, 這意味着當你須要把它讀取回來時你須要整個對象圖的一個拷貝。這就是當你深拷貝一個對象時真正須要的東西。請注意,當你經過序列化進行深拷貝時,必須確保對象圖中全部類都是可序列化的。

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; 
   } 
}
複製代碼
public class DeepCopy {

   public static void main(String[] args) throws IOException { 
      ObjectOutputStream oos = null; 
      ObjectInputStream ois = null; 

      try { 
         // 建立原始的可序列化對象 
         ColoredCircle c1 = new ColoredCircle(100, 100); 
         System.out.println("Original = " + c1); 

         ColoredCircle 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 = (ColoredCircle) ois.readObject(); 

         // 校驗內容是否相同 
         System.out.println("Copied = " + c2); 
         // 改變原始對象的內容 
         c1.setX(200); 
         c1.setY(200); 
         // 查看每個如今的內容 
         System.out.println("Original = " + c1); 
         System.out.println("Copied = " + c2); 
      } catch (Exception e) { 
         System.out.println("Exception in main = " + e); 
      } finally { 
         oos.close(); 
         ois.close(); 
      } 
   } 
}

複製代碼

​ 輸出結果以下:

Original = x=100, y=100
Copied   = x=100, y=100
Original = x=200, y=200
Copied   = x=100, y=100
複製代碼

這裏,你只須要作如下幾件事兒:

  • 確保對象圖中的全部類都是可序列化的
  • 建立輸入輸出流
  • 使用這個輸入輸出流來建立對象輸入和對象輸出流
  • 將你想要拷貝的對象傳遞給對象輸出流
  • 從對象輸入流中讀取新的對象而且轉換回你所發送的對象的類

在這個例子中,我建立了一個ColoredCircle對象c1而後將它序列化(將它寫到ByteArrayOutputStream中). 而後我反序列化這個序列化後的對象並將它保存到c2中。隨後我修改了原始對象c1。而後結果如你所見,c1不一樣於c2,對c1所作的任何修改都不會影響c2。

注意: 序列化這種方式有其自身的限制和問題:

  1. 由於沒法序列化transient變量, 使用這種方法將沒法拷貝transient變量。

  2. 再就是性能問題。建立一個socket, 序列化一個對象, 經過socket傳輸它,而後反序列化它,這個過程與調用已有對象的方法相比是很慢的。因此在性能上會有天壤之別。若是性能對你的代碼來講是相當重要的,建議不要使用這種方式。它比經過實現Clonable接口這種方式來進行深拷貝幾乎多花100倍的時間。

4. 延遲拷貝

  延遲拷貝是淺拷貝和深拷貝的一個組合,實際上不多會使用。 當最開始拷貝一個對象時,會使用速度較快的淺拷貝,還會使用一個計數器來記錄有多少對象共享這個數據。當程序想要修改原始的對象時,它會決定數據是否被共享(經過檢查計數器)並根據須要進行深拷貝。

  延遲拷貝從外面看起來就是深拷貝,可是隻要有可能它就會利用淺拷貝的速度。當原始對象中的引用不常常改變的時候可使用延遲拷貝。因爲存在計數器,效率降低很高,但只是常量級的開銷。並且, 在某些狀況下, 循環引用會致使一些問題。

5. 如何選擇

  若是對象的屬性全是基本類型的,那麼可使用淺拷貝,可是若是對象有引用屬性,那就要基於具體的需求來選擇淺拷貝仍是深拷貝。個人意思是若是對象引用任什麼時候候都不會被改變,那麼不必使用深拷貝,只須要使用淺拷貝就好了。若是對象引用常常改變,那麼就要使用深拷貝。沒有一成不變的規則,一切都取決於具體需求。

原文出處


相關文章
相關標籤/搜索