1. 程式人生 > >Java clone、淺複製、深複製、複製建構函式

Java clone、淺複製、深複製、複製建構函式

在java中的物件重複利用通常有兩種渠道:複製引用、克隆,不管何種方法,它們都是為了減少物件重複建立和賦值操作,一定程度上提高效率。這裡就有關物件複用的幾種方式和關係進行探討。

共識

java中的物件分為兩派:值型別和引用型別,這是因為他們的傳遞方式,一個是值傳遞,一個是引用傳遞。

對於值型別,因為是值傳遞,所以在使用值型別的時候無須考慮引用型別存在一些問題,如:equals,hashcode,nullpoint,而在這裡關鍵無須考慮的問題是:複製問題。諸如y = 1;x=y,y=2這些happend-befored,值型別的值是不受過去和將來影響的。

而對於引用型別就沒這麼輕鬆了,大家也有目共睹,於是在複製問題上,就有了題目中列出的方式。

Clone

User u1 = new User(“”,“”,“”,“”)

User u1 = new User(“”,“”,“”,“”)

像上面這種拙劣的物件建立方式,是我們不希望看到的。JAVA提供了物件克隆方法,這個是Object類的本地方法,利用克隆可以快速高效的複製一個物件而無需重新建立:

public class User implements Cloneable{
	String name = "origin";
	
	
	@Override
	protected Object clone() throws CloneNotSupportedException {
		return super.clone();
	}
@Test
	public void testClone() throws CloneNotSupportedException {
		User u1 = new User();
		User u2 = (User) u1.clone();
		System.out.println(u1.hashCode());
		System.out.println(u1.name);
		
		u1.name = "origin2";
		System.out.println(u2.hashCode());
		System.out.println(u2.name);
		
	}

輸出:

1607460018

origin

48612937

origin

可以看出,克隆會創建出一個新的物件,物件的成員具有原始物件的資訊,並且成員都是新的記憶體分配,不受happend-before的影響。要使用clone方法,必需實現Cloneable介面,該介面意義和serializable類似,然後重寫Object的clone方法並呼叫super的本地方法。

淺複製

不過這看起來似乎挺美好的背後,並不是如你所想的那樣,我們給user加一個複合引用型別的Inner:

public class User implements Cloneable{
	String name = "origin";
	Inner inner;
	
	public User() {
		inner = new Inner();
		inner.name = "inner-origin";
	}

這個Inner類同樣持有一些引用型別的成員:

public class Inner{
	String name;
}
@Test
	public void testClone() throws CloneNotSupportedException {
		User u1 = new User();
		User u2 = (User) u1.clone();
		
		System.out.println(u1.hashCode());
		System.out.println(u1.name);
		System.out.println(u1.inner.name);
		
		u1.name = "origin2";
		u1.inner.name = "innerorigin2";
		
		System.out.println(u2.hashCode());
		System.out.println(u2.name);
		System.out.println(u2.inner.name);

輸出:

1607460018
origin
inner-origin
48612937
origin

innerorigin2

顯然:如果成員是另一個複合型別的引用,那麼這個成員還是受到happend-before影響,也就是說,使用clone只能做到表面功夫,無法對更深層的引用進行記憶體層面上的複製,因此,這種複製方式被稱為:淺複製。

深複製

那麼現在問題來了,你應該也知道了深複製應該就是相對於淺複製,實現一個物件由外到內的全面克隆。如何做到深複製,java不像C語言那樣可以通過operator操作來進行值拷貝,因此沒有辦法做到絕對意義上的深複製,可以說是沒有這個概念。那麼,這裡說的深複製是在一定前提下進行的,就可以達到深複製的效果。

深複製方式1:物件序列化反序列化

public class User implements Cloneable,Serializable{
	String name = "origin";
	Inner inner;
	
	public User() {
		inner = new Inner();
		inner.name = "inner-origin";
	}
	
	@Override
	protected Object clone() throws CloneNotSupportedException {
		return super.clone();
	}
	
	public Object deepCopy(User u) throws IOException, ClassNotFoundException {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(out);
		oos.writeObject(u);
		InputStream in = new ByteArrayInputStream(out.toByteArray());
		ObjectInputStream ois = new ObjectInputStream(in);
		return ois.readObject();
		
	}

同時,要求引用型別的成員也要實現序列化介面:

public class Inner implements Serializable{
	String name;
}
public class TestClone {
	
	@Test
	public void testClone() throws CloneNotSupportedException, ClassNotFoundException, IOException {
		User u1 = new User();
		User u2 = (User) u1.clone();
		User u3 = (User) u1.deepCopy(u1);
		
		u1.name = "origin2";
		u1.inner.name = "innerorigin2";
		
		System.out.println(u1.hashCode());
		System.out.println(u1.inner.hashCode());
		System.out.println(u1.name);
		System.out.println(u1.inner.name);
		
//		System.out.println(u2.hashCode());
//		System.out.println(u2.name);
//		System.out.println(u2.inner.name);
		
		System.out.println(u3.hashCode());
		System.out.println(u3.inner.hashCode());
		System.out.println(u3.name);
		System.out.println(u3.inner.name);
	}

輸出:

1607460018
1811075214
origin2
innerorigin2
48612937
325333723
origin
inner-origin

可以看到深複製的目的已經達成,因為通過序列化和反序列化將分配新的記憶體建立物件和成員,所以這種做法是有效的,然而效率卻很低下,畢竟不能稱為複製,只是一種婉轉曲折的傳輸方式,比較取巧,但是一些ORM框架仍然不得不採用這種做法,導致效率低下。

深複製方式2:複製函式

觀察這兩個User的建構函式有什麼不同?

	public User(String name,Inner inner) {
		this.name = name;
		this.inner = inner;
	}
	
	public User(User user) {
		this.name = user.name;
		this.inner = new Inner();
		inner.name = user.inner.name;
	}

第一個是有參的建構函式,第二個是用於複製的建構函式,所以也稱為複製建構函式,用於從一個相同型別的物件中複製成員變數。這個例子就不測試了,相信你也能看出,其實就是多了一道工序來建立新的成員變數inner,而不是引用原來的inner物件,效率自然會比序列化和反序列化要高,只不過深度有限,不能保證複製深度範圍外的物件深複製。如果你確定成員中的引用型別也能保證(或不需要關心)不變性,那麼就可以通這種方式做一個不完全的深複製,在效率和深度上做一個平衡。

效率

下面做了一個簡單的測試,可以看出淺複製和深複製的差距:

	@Test
	public void testCopy() throws CloneNotSupportedException {
		for(int i=0;i<100000;i++) {
			User u1 = new User();
			User u2 = (User) u1.clone();	
		}
	}
	
	@Test
	public void testDeepCopy() throws CloneNotSupportedException, ClassNotFoundException, IOException {
		for(int i=0;i<100000;i++) {
			User u1 = new User();
			User u3 = (User) u1.deepCopy(u1);
		}
	}
	
	@Test
	public void testDeepCopyByConstructor() {
		for(int i=0;i<100000;i++) {
			User u1 = new User();
			User u3 = new User(u1);
		}
	}