Object類clone方法深度解析
首先我們來看下clone方法的原始碼
protected native Object clone() throws CloneNotSupportedException;
通過原始碼可以發現幾點:
- clone方法是native方法,native方法的效率遠高於非native方法,因此還是使用clone方法去做物件的拷貝而不是使用new的方法,copy。
- 此方法被protected修飾。這就意味著想要使用,必須繼承它(廢話,預設都是繼承的)。然後過載它,如果想要使得其他類能使用這個類,需要設定成public。
- 返回值是一個Object物件,所以要強制轉換才行。
測試clone方法,程式碼如下:
public class TestReen{
public static void main(String[] args) throws Exception{
TestReen tReen = new TestReen();
TestReen copy = (TestReen)tReen.clone();
}
}
但是執行時發現丟擲異常了。
Exception in thread "main" java.lang.CloneNotSupportedException: com.test.count.TestReen at java.lang.Object.clone(Native Method) at com.test.count.TestReen.main(TestReen.java:11)
進入clone方法,看註釋發現如下資訊:表明此類不支援Cloneable介面的話就會報錯。
@exception CloneNotSupportedException if the object's class does not * support the {@code Cloneable} interface. Subclasses * that override the {@code clone} method can also * throw this exception to indicate that an instance cannot * be cloned.
去看看Cloneable介面,竟然是個空的介面
public interface Cloneable {
}
總結其註釋的話,差不多三點:
- 此類實現了Cloneable介面,以指示Object的clone()方法可以合法地對該類例項進行按欄位複製;
- 如果在沒有實現Cloneable介面的例項上呼叫Object的clone()方法,則會導致丟擲CloneNotSupporteddException;
- 按照慣例,實現此介面的類應該使用公共方法重寫Object的clone()方法,Object的clone()方法是一個受保護的方法;
因此想實現clone的話,除了繼承Object類外,還需要實現Cloneable介面;
建立並返回此物件的一個副本。對於任何物件x,表示式:
- x.clone() != x為true
- x.clone().getClass() == x.getClass()為true
- x.clone().equals(x)一般情況下為true,但這並不是必須要滿足的要求
測試以下例子:
public class TestReen implements Cloneable{
private int id;
private static int i = 1;
public TestReen() {
System.out.println("i = " + i);
System.out.println("執行了建構函式"+i);
i++;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public static void main(String[] args) throws Exception{
TestReen t1 = new TestReen();
t1.setId(1);
TestReen t2 = (TestReen)t1.clone();
TestReen t3 = new TestReen();
System.out.println("t1 id: "+ t1.getId());
System.out.println("t2 id: "+ t2.getId());
System.out.println("t1 == t2 ? " + (t1 == t2));
System.out.println("t1Class == t2Class ? " + (t1.getClass() == t2.getClass()));
System.out.println("t1.equals(t2) ? " + t1.equals(t2));
}
}
結果如下,有幾個發現:
- 建構函式除了new執行以外,clone並沒有呼叫到建構函式,也就是clone方法是不走構造方法的。
- t1 和 t2 是不等的,說明指向了不同的堆地址空間,是兩個物件。
- getClass是相同的,getClass是什麼?是獲取這個例項的型別類,有點拗口,其實就是TestReen型別,可見clone出來的型別還是一樣的。
i = 1
執行了建構函式1
i = 2
執行了建構函式2
t1 id: 1
t2 id: 1
t1 == t2 ? false
t1Class == t2Class ? true
t1.equals(t2) ? false
克隆分為淺克隆(shallow clone)和 深克隆(deep clone)。
- 淺克隆:Object的clone提供的就是淺克隆,由下面的例子可以看見,只克隆了自身物件和物件內例項變數的地址引用,它內部的例項變數還是指向原先的堆記憶體區域。
- 深克隆:克隆所有的物件,包括自身以及自身內部物件。
我們來看下什麼是淺克隆,什麼是深克隆。例如我有一個Person類:
public class Person implements Cloneable{
private int age ;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public Person() {}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return (Person)super.clone();
}
}
由於age是基本資料型別, 那麼對它的拷貝沒有什麼疑議,直接將一個4位元組的整數值拷貝過來就行。但是name是String型別的, 它只是一個引用, 指向一個真正的String物件,那麼對它的拷貝有兩種方式: 直接將源物件中的name的引用值拷貝給新物件的name欄位, 或者是根據原Person物件中的name指向的字串物件建立一個新的相同的字串物件,將這個新字串物件的引用賦給新拷貝的Person物件的name欄位。這兩種拷貝方式分別叫做淺拷貝和深拷貝。深拷貝和淺拷貝的原理如下圖所示:
我們先來看下一個淺克隆的例子:
public class TestReen implements Cloneable{
private String name;
private SonReen sonReen;
public TestReen(String name, SonReen sonReen) {
this.name = name;
this.sonReen = sonReen;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public SonReen getSonReen() {
return sonReen;
}
public void setSonReen(SonReen sonReen) {
this.sonReen = sonReen;
}
public static void main(String[] args) throws Exception {
SonReen sonReen = new SonReen("abc");
TestReen t1 = new TestReen("李四", sonReen);
TestReen t2 = (TestReen) t1.clone();
System.out.println("t1==t2 ? " + (t1 == t2));
System.out.println("t1.name==t2.name ? " + (t1.getName() == t2.getName()));
System.out.println("t1.sonReen==t2.sonReen ? " + (t1.getSonReen() == t2.getSonReen()));
System.out.println("t1.sonReen.SonName==t2.sonReen.SonName ? "
+ (t1.getSonReen().getSonName() == t2.getSonReen().getSonName()));
System.out.println("========================================");
t1.getSonReen().setSonName("王五");
System.out.println(t1.getSonReen().getSonName());
System.out.println(t2.getSonReen().getSonName());
}
}
class SonReen{
private String sonName;
public SonReen(String sonName) {
super();
this.sonName = sonName;
}
public String getSonName() {
return sonName;
}
public void setSonName(String sonName) {
this.sonName = sonName;
}
}
結果如下:
t1==t2 ? false
t1.name==t2.name ? true
t1.sonReen==t2.sonReen ? true
t1.sonReen.SonName==t2.sonReen.SonName ? true
========================================
王五
王五
我們從結果看出,只有t1和t2指向的是不同的地址,而t1的sonReen和t2的sonReen指向的是同一塊地址,說明淺克隆在克隆一個類的引用型別的成員變數時,克隆的是引用。並且我們修改t1的sonReen的Sonname值的時候,我們發現t2的這個值也被修改了,這更加說明 t1的sonReen和t2的sonReen指向的是同一塊地址。
深克隆一般有兩種實現方式:
- 讓類實現序列化介面Serializable。然後對物件進行序列化操作,然後反序列化得到物件。
- 先呼叫super.clone()方法克隆出一個新物件來,然後再呼叫子物件的clone方法實現深度克隆。
我們先看第一種:
public class TestReen implements Serializable {
private String name;
private SonReen sonReen;
public TestReen(String name, SonReen sonReen) {
this.name = name;
this.sonReen = sonReen;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public SonReen getSonReen() {
return sonReen;
}
public void setSonReen(SonReen sonReen) {
this.sonReen = sonReen;
}
public Object deepClone() throws Exception {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bo);
out.writeObject(this);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return oi.readObject();
}
public static void main(String[] args) throws Exception {
SonReen sonReen = new SonReen("abc");
TestReen t1 = new TestReen("李四", sonReen);
TestReen t2 = (TestReen) t1.deepClone();
System.out.println("t1==t2 ? " + (t1 == t2));
System.out.println("t1.name==t2.name ? " + (t1.getName() == t2.getName()));
System.out.println("t1.sonReen==t2.sonReen ? " + (t1.getSonReen() == t2.getSonReen()));
System.out.println("t1.sonReen.SonName==t2.sonReen.SonName ? "
+ (t1.getSonReen().getSonName() == t2.getSonReen().getSonName()));
System.out.println("========================================");
t1.setName("王五");
System.out.println(t1.getName());
System.out.println(t2.getName());
}
}
class SonReen implements Serializable {
private String sonName;
public SonReen(String sonName) {
super();
this.sonName = sonName;
}
public String getSonName() {
return sonName;
}
public void setSonName(String sonName) {
this.sonName = sonName;
}
}
結果為:
t1==t2 ? false
t1.name==t2.name ? false
t1.sonReen==t2.sonReen ? false
t1.sonReen.SonName==t2.sonReen.SonName ? false
========================================
王五
李四
我們從結果可以看到,通過序列化和反序列化。克隆出來的t2與t1並不是一個物件,並且t2和t1內部的引用型別的成員變數也得到了克隆,不再是想淺度克隆那樣克隆引用。並且我們還可以發現,通過修改t1的name屬性,t2的name屬性並沒有發生變化。更加說明了t1的name和t2的name指向了不同的記憶體單元。這就是深度克隆。
第二種:
class Body implements Cloneable {
public Head head;
public Body() {
}
public Body(Head head) {
this.head = head;
}
protected Object clone() throws CloneNotSupportedException {
Body newBody = (Body) super.clone();
newBody.head = (Head) head.clone();
return newBody;
}
}
class Head implements Cloneable {
public Head() {
}
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class TestReen {
public static void main(String[] args) throws CloneNotSupportedException {
Body body1 = new Body(new Head());
Body body2 = (Body) body1.clone();
System.out.println("body1 == body2 : " + (body1 == body2));
System.out.println("body1.head == body2.head : " + (body1.head == body2.head));
}
}
結果如下:
body1 == body2 : false
body1.head == body2.head : false
從結果我們看出這也是一個深度克隆,但是要注意的是,如果Head中假設有一個Face型別,如要深度克隆,那麼這個Face型別也要實現Cloneable介面並且重寫clone方法。