1. 程式人生 > >Object類clone方法深度解析

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指向的是同一塊地址。

深克隆一般有兩種實現方式:

  1. 讓類實現序列化介面Serializable。然後對物件進行序列化操作,然後反序列化得到物件。
  2. 先呼叫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方法。