Java基礎——clone()方法淺析
一、clone的概念
clone顧名思義就是複製, 在Java語言中, clone方法被物件呼叫,所以會複製物件。所謂的複製物件,首先要分配一個和源物件同樣大小的空間,在這個空間中建立一個新的物件。那麼在java語言中,有幾種方式可以建立物件呢?
- 使用new操作符建立一個物件
- 使用clone方法複製一個物件
那麼這兩種方式有什麼相同和不同呢?
- new操作符的本意是分配記憶體。程式執行到new操作符時, 首先去看new操作符後面的型別,因為知道了型別,才能知道要分配多大的記憶體空間。分配完記憶體之後,再呼叫建構函式,填充物件的各個域,這一步叫做物件的初始化,構造方法返回後,一個物件建立完畢,可以把他的引用(地址)釋出到外部,在外部就可以使用這個引用操縱這個物件。 1 Person p = new Person(23, "zhang"); 2 Person p1 = p; 3 4 System.out.println(p); 5 System.out.println(p1);
當Person p1 = p;執行之後, 是建立了一個新的物件嗎? 首先看列印結果:
com.pansoft.zhangjg.testclone.Person@2f9ee1ac
com.pansoft.zhangjg.testclone.Person@2f9ee1ac
可以看出,列印的地址值是相同的,既然地址都是相同的,那麼肯定是同一個物件。p和p1只是引用而已,他們都指向了一個相同的物件Person(23, "zhang") 。 可以把這種現象叫做引用的複製
- 而clone在第一步是和new相似的, 都是分配記憶體,呼叫clone方法時,分配的記憶體和源物件(即呼叫clone方法的物件)相同,然後再使用原物件中對應的各個域,填充新物件的域, 填充完成之後,clone方法返回,一個新的相同的物件被建立,同樣可以把這個新物件的引用釋出到外部。注意:如果一個類沒有複寫clone()方法,則clone()方法預設的是返回一個Object物件,我們可以強制轉化為我們需要的類。 Person p = new Person(23, "zhang"); Person p1 = (Person) p.clone(); System.out.println(p); System.out.println(p1); 從列印結果可以看出,兩個物件的地址是不同的,也就是說建立了新的物件, 而不是把原物件的地址賦給了一個新的引用變數: com.pansoft.zhangjg.testclone.Person@2f9ee1ac com.pansoft.zhangjg.testclone.Person@67f1fba0
二、clone的用法
Java程式設計中經常會用到clone()方法,clone()方法是Object類的一個protected的方法,由於非靜態所以不能直接呼叫,但是可以被子類呼叫。Object類的clone()方法會知道物件大小,為其分配足夠的記憶體空間,並將其物件的內容複製到新的物件中。但是,clone()方法在執行其動作之前必須先檢查class是否實現了Cloneable介面。Cloneable介面和Serialiable介面一樣是一個標記介面,沒有任何內容。
Java中clone()方法的含義是,假設x是一個非空的物件,則:
x.clone() != x 為true
x.clone(),getClass() == x.getClass() 為true,說明他們是同一型別的class
x.equals(x.clone()) 為true,說明邏輯上應該相同
一個類的物件要想在呼叫clone()方法時不丟擲CloneNotSupportedException,有兩種方法:
- 該類在定義宣告時宣告實現了Cloneable介面,即在宣告時加上“implements Cloneable”即可(淺拷貝)
- 該類在定義時,宣告實現Cloneable介面,並且複寫clone()方法,並將其宣告為public(深拷貝)
三、淺析clone()方法與淺拷貝、深拷貝概念
在上面一節中我們發現正確使用clone()方法的兩種方法的結果並不相同,直接實現Cloneable介面只是實現了淺拷貝,而複寫clone()方法則可以實現深拷貝。那麼,這種copy有什麼區別呢?
上面的示例程式碼中,Person中有兩個成員變數,分別是name和age, name是String型別, age是int型別。程式碼非常簡單,如下所示:
1 public class Person implements Cloneable{
2
3 private int age ;
4 private String name;
5
6 public Person(int age, String name) {
7 this.age = age;
8 this.name = name;
9 }
10
11 public Person() {}
12
13 public int getAge() {
14 return age;
15 }
16
17 public String getName() {
18 return name;
19 }
20
21 @Override
22 protected Object clone() throws CloneNotSupportedException {
23 return (Person)super.clone();
24 }
25 }
由於age是基本資料型別, 那麼對它的拷貝沒有什麼疑議,直接將一個4位元組的整數值拷貝過來就行。但是name是String型別的, 它只是一個引用, 指向一個真正的String物件,那麼對它的拷貝有兩種方式: 直接將源物件中的name的引用值拷貝給新物件的name欄位, 或者是根據原Person物件中的name指向的字串物件建立一個新的相同的字串物件,將這個新字串物件的引用賦給新拷貝的Person物件的name欄位。這兩種拷貝方式分別叫做淺拷貝和深拷貝。深拷貝和淺拷貝的原理如下圖所示:
下面通過程式碼進行驗證。如果兩個Person物件的name的地址值相同, 說明兩個物件的name都指向同一個String物件, 也就是淺拷貝, 而如果兩個物件的name的地址值不同, 那麼就說明指向不同的String物件, 也就是在拷貝Person物件的時候, 同時拷貝了name引用的String物件, 也就是深拷貝。驗證程式碼如下:
Person p = new Person(23, "zhang");
Person p1 = (Person) p.clone();
System.out.println("p.getName().hashCode() : " + p.getName().hashCode());
System.out.println("p1.getName().hashCode() : " + p1.getName().hashCode());
String result = p.getName().hashCode() == p1.getName().hashCode()
? "clone是淺拷貝的" : "clone是深拷貝的";
System.out.println(result);
列印結果為:
p.getName().hashCode() : 115864556 p1.getName().hashCode() : 115864556 clone是淺拷貝的
所以,clone方法執行的是淺拷貝, 在編寫程式時要注意這個細節。
四、深拷貝的實現
為了要在clone物件時進行深拷貝, 那麼就要實現Clonable介面,覆蓋並實現clone方法,除了呼叫父類中的clone方法得到新的物件, 還要將該類中的引用變數也clone出來。如果只是用Object中預設的clone方法,是淺拷貝的,再次以下面的程式碼驗證:
1 static class Body implements Cloneable{
2 public Head head;
3
4 public Body() {}
5
6 public Body(Head head) {this.head = head;}
7
8 @Override
9 protected Object clone() throws CloneNotSupportedException {
10 return super.clone();
11 }
12
13 }
14 static class Head /*implements Cloneable*/{
15 public Face face;
16
17 public Head() {}
18 public Head(Face face){this.face = face;}
19
20 }
21 public static void main(String[] args) throws CloneNotSupportedException {
22
23 Body body = new Body(new Head());
24
25 Body body1 = (Body) body.clone();
26
27 System.out.println("body == body1 : " + (body == body1) );
28
29 System.out.println("body.head == body1.head : " + (body.head == body1.head));
30
32 }
在以上程式碼中, 有兩個主要的類, 分別為Body和Face, 在Body類中, 組合了一個Face物件。當對Body物件進行clone時, 它組合的Face物件只進行淺拷貝。列印結果可以驗證該結論:
body == body1 : false body.head == body1.head : true
如果要使Body物件在clone時進行深拷貝, 那麼就要在Body的clone方法中,將源物件引用的Head物件也clone一份。
1 static class Body implements Cloneable{
2 public Head head;
3 public Body() {}
4 public Body(Head head) {this.head = head;}
5
6 @Override
7 protected Object clone() throws CloneNotSupportedException {
8 Body newBody = (Body) super.clone();
9 newBody.head = (Head) head.clone();
10 return newBody;
11 }
12
13 }
14 static class Head implements Cloneable{
15 public Face face;
16
17 public Head() {}
18 public Head(Face face){this.face = face;}
19 @Override
20 protected Object clone() throws CloneNotSupportedException {
21 return super.clone();
22 }
23 }
24 public static void main(String[] args) throws CloneNotSupportedException {
25
26 Body body = new Body(new Head());
27
28 Body body1 = (Body) body.clone();
29
30 System.out.println("body == body1 : " + (body == body1) );
31
32 System.out.println("body.head == body1.head : " + (body.head == body1.head));
33
34
35 }
列印結果為:
body == body1 : false body.head == body1.head : true 由此可見, body和body1內的head引用指向了不同的Head物件, 也就是說在clone Body物件的同時, 也拷貝了它所引用的Head物件, 進行了深拷貝。
但是,我們可以看到,Head類中的face物件的clone()是淺複製,所以這裡肯定還會存在一個結果是body.head.face == body1.head.face : false,這表明這樣寫的clone()並不是很徹底,那麼要進行完全徹底的深複製,我們應該寫成這樣:
1 static class Head implements Cloneable{
2 public Face face;
3
4 public Head() {}
5 public Head(Face face){this.face = face;}
6 @Override
7 protected Object clone() throws CloneNotSupportedException {
8 //return super.clone();
9 Head newHead = (Head) super.clone();
10 newHead.face = (Face) this.face.clone();
11 return newHead;
12 }
13 }
14
15 static class Face implements Cloneable{
16 @Override
17 protected Object clone() throws CloneNotSupportedException {
18 return super.clone();
19 }
20 }
我們需要對物件中的每一個引用進行深拷貝才能徹底實現深拷貝。