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