[Java基礎] 深拷貝與淺拷貝

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


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

淺拷貝是指拷貝對象時僅僅拷貝對象本身(包括對象中的基本變量),而不拷貝對象包含的引用指向的對象。深拷貝不僅拷貝對象本身,而且拷貝對象包含的引用指向的所有對象。舉例來說更加清楚:對象A1中包含對B1的引用,B1中包含對C1的引用。淺拷貝A1得到A2,A2 中依然包含對B1的引用,B1中依然包含對C1的引用。深拷貝則是對淺拷貝的遞歸,深拷貝A1得到A2,A2中包含對B2(B1的copy)的引用,B2 中包含對C2(C1的copy)的引用。

若不對clone()方法進行改寫,則調用此方法得到的對象即爲淺拷貝。

二、淺拷貝

1、什麼是淺拷貝
   淺拷貝是按位拷貝對象,它會創建一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值;如果屬性是內存地址(引用類型),拷貝的就是內存地址 ,因此如果其中一個對象改變了這個地址,就會影響到另一個對象。


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

2、如何實現淺拷貝

<span style="font-size:14px;">package objectCopy;

class Subject {
   private String name; 
   public Subject(String s) { 
      name = s; 
   } 
   public String getName() { 
      return name; 
   } 
   public void setName(String s) { 
      name = s; 
   } 
}
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());
	}
}</span>

輸出結果如下: 
 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。

三、深拷貝
1、什麼是深拷貝
   深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的內存。當對象和它所引用的對象一起拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢並且花銷較大。



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

2、如何實現深拷貝

<span style="font-size:14px;">package objectCopy;

class Subject {
   private String name; 
   public Subject(String s) { 
      name = s; 
   } 
   public String getName() { 
      return name; 
   } 
   public void setName(String s) { 
      name = s; 
   } 
}
/*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; 
      } 
   } 
}*/

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

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());
	}
}</span>

輸出結果如下:
  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、通過序列化實現深拷貝
    也可以通過序列化來實現深拷貝。序列化是幹什麼的?它將整個對象圖寫入到一個持久化存儲文件中並且當需要的時候把它讀取回來, 這意味着當你需要把它讀取回來時你需要整個對象圖的一個拷貝。這就是當你深拷貝一個對象時真正需要的東西。請注意,當你通過序列化進行深拷貝時,必須確保對象圖中所有類都是可序列化的。

<span style="font-size:14px;">package objectCopy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class ColoredCircle implements Serializable {
	/**
	 * 生成序列化UID
	 */
	private static final long serialVersionUID = 3306829010445297199L;
	
	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 SerializableTest {

	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();
		}
	}
}</span>

輸出結果如下:
Original = x=100, y=100 
Copied = x=100, y=100 
Original = x=200, y=200 
Copied = x=100, y=100
這裏,你只需要做以下幾件事兒:
(1)確保對象圖中的所有類都是可序列化的
(2)創建輸入輸出流
(3)使用這個輸入輸出流來創建對象輸入和對象輸出流
(4)將你想要拷貝的對象傳遞給對象輸出流
(5)從對象輸入流中讀取新的對象並且轉換回你所發送的對象的類

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

注意,序列化這種方式有其自身的限制和問題:
因爲無法序列化transient變量, 使用這種方法將無法拷貝transient變量。

   再就是性能問題。創建一個socket, 序列化一個對象, 通過socket傳輸它, 然後反序列化它,這個過程與調用已有對象的方法相比是很慢的。所以在性能上會有天壤之別。如果性能對你的代碼來說是至關重要的,建議不要使用這種方式。它比通過實現Clonable接口這種方式來進行深拷貝幾乎多花100倍的時間。

四、延遲拷貝
   延遲拷貝是淺拷貝和深拷貝的一個組合,實際上很少會使用。 當最開始拷貝一個對象時,會使用速度較快的淺拷貝,還會使用一個計數器來記錄有多少對象共享這個數據。當程序想要修改原始的對象時,它會決定數據是否被共享(通過檢查計數器)並根據需要進行深拷貝。 
   延遲拷貝從外面看起來就是深拷貝,但是只要有可能它就會利用淺拷貝的速度。當原始對象中的引用不經常改變的時候可以使用延遲拷貝。由於存在計數器,效率下降很高,但只是常量級的開銷。而且, 在某些情況下, 循環引用會導致一些問題。

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



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