1. 程式人生 > >effective java(11) 之謹慎地覆蓋clone

effective java(11) 之謹慎地覆蓋clone

effective java 之謹慎地覆蓋clone
1、Cloneable介面表明這樣的物件是允許克隆的,但這個介面並沒有成功達到這個目的,主要是因為它缺少一個clone方法,Object的clone方法是受保護的。
如果不借助反射,就不能僅僅因為一個物件實現了Colneable就可以呼叫clone方法,即使是反射呼叫也不能保證這個物件一定具有可訪問clone方法。

2、Cloneable並沒有包含任何方法,那麼它到底有什麼用呢?
它其實決定了Object中受保護的clone方法實現的行為,如果一個類實現了Cloneable那麼Object的clone方法就返回該物件的逐域拷貝,否則會丟擲CloneNotSupportedException。
但真說介面一種極端非典型用法,不值得提倡。

3、通常情況下,實現介面是為了表明類可以為它的客戶做些什麼,然而對於Cloneable介面改變了超類中受保護的方法的行為。

4、如果實現Cloneable介面是要對某個類起到作用,類和它的所有超類都必須遵守一個一定協議,言外之意就是無需呼叫構造器就可以建立物件。

5、Object中Clone方法的通用約定:

Clone方法用於建立和返回物件的一個拷貝,一般含義如下:
1、對於任何物件x,表示式 x.clone()!=x 將會是true,並且表示式 x.clone().getClass() == x.getClass()將會是true,但這不是絕對要求。
2、通常情況下,表示式 x.clone.equals(x)將會是true,同1一樣這不是絕對要求。
拷貝物件往往會導致建立它的類的一個新例項,但它同時也要求拷貝內部的資料介面,這個過程中沒有呼叫構造器。


6、一個淺克隆的例子:
	public class Student implements Cloneable {
		private String name;
		private int age;


		public Student(String name, int age) {
			this.name = name;
			this.age = age;
		}
		public Object clone() {
			Object o = null;
			try {
				o = (Student) super.clone();// Object 中的clone()識別出你要複製的是哪個物件
			} catch (CloneNotSupportedException e) {
				System.out.println(e.toString());
			}
			return o;
		}
		public static void main(String[] args) {
			Student s1 = new Student("zhangsan", 18);
			Student s2 = (Student) s1.clone();
			System.out.println("克隆後s2:name=" + s2.name + "," + "age=" + s2.age);
			s2.name = "lisi";
			s2.age = 20;
			// 修改學生2後,不影響學生1的值。
			System.out.println("克隆修改後s1:name=" + s1.name + "," + "age=" + s1.age);
			System.out.println("克隆修改後s2:name=" + s2.name + "," + "age=" + s2.age);
		}
	}




克隆後s2:name=zhangsan,age=18
克隆修改後s1:name=zhangsan,age=18
克隆修改後s2:name=lisi,age=20


如果類的每個域包含一個基本型別的值,或者包含一個指向不可變物件的引用,那麼被返回的物件則正是所需要的物件,只需要簡單地呼叫super.clone() 而不用做進一步的處理。
但是如果類指向的其他物件的引用是可變的時,那麼只是簡單的clone就無法做到完全的克隆了。


7、對引用的克隆:
	public class Student implements Cloneable {
		private String name;
		private int age;
		Professor p;// 這裡學生1和學生2的引用值都是一樣的。


		public Student(String name, int age, Professor p) {
			super();
			this.name = name;
			this.age = age;
			this.p = p;
		}
		public Object clone() {
			Object o = null;
			try {
				o = (Student) super.clone();// Object 中的clone()識別出你要複製的是哪個物件
			} catch (CloneNotSupportedException e) {
				System.out.println(e.toString());
			}
			return o;
		}
		public static void main(String[] args) {
			Professor p = new Professor("wangwu", 50);
			Student s1 = new Student("zhangsan", 18, p);
			Student s2 = (Student) s1.clone();
			System.out.println("克隆後s1:name=" + s1.p.name + "," + "age=" + s1.p.age);
			System.out.println("克隆後s2:name=" + s2.p.name + "," + "age=" + s2.p.age);
			s2.p.name = "lisi";
			s2.p.age = 30;
			// 修改克隆的值,原來的值也會變化,引用的是同一個值。
			System.out.println("克隆後s1:name=" + s1.p.name + "," + "age=" + s1.p.age);
			System.out.println("克隆後s2:name=" + s2.p.name + "," + "age=" + s2.p.age);
		}
	}
	class Professor {
		String name;
		int age;


		Professor(String name, int age) {
			this.name = name;
			this.age = age;
		}
	}

克隆後s1:name=wangwu,age=50
克隆後s2:name=wangwu,age=50
克隆後s1:name=lisi,age=30
克隆後s2:name=lisi,age=30

s2對s1進行克隆時,對s1的屬性Professor p並沒有進行克隆,導致s1和s2對其引用指向同一個,這會造成s2若改變了值,s1則也被動改變了。
如何做到修改s2的教授不會影響s1的教授。需要深度克隆。

9、深拷貝例子:
	public class Student implements Cloneable {
		private String name;
		private int age;
		Professor p;// 學生1和學生2的引用值都是一樣的。

		public Student(String name, int age, Professor p) {
			super();
			this.name = name;
			this.age = age;
			this.p = p;
		}
		public Object clone() {
			Student o = null;
			try {
				o = (Student) super.clone();// Object 中的clone()識別出你要複製的是哪個物件
			} catch (CloneNotSupportedException e) {
				System.out.println(e.toString());
			}
			o.p = (Professor) p.clone();
			return o;
		}
		public static void main(String[] args) {
			Professor p = new Professor("wangwu", 50);
			Student s1 = new Student("zhangsan", 18, p);
			Student s2 = (Student) s1.clone();
			System.out.println("克隆後s1:name=" + s1.p.name + "," + "age=" + s1.p.age);
			System.out.println("克隆後s2:name=" + s2.p.name + "," + "age=" + s2.p.age);
			s2.p.name = "lisi";
			s2.p.age = 30;
			// 修改學生2後,不影響學生1的值。
			System.out.println("克隆後s1:name=" + s1.p.name + "," + "age=" + s1.p.age);
			System.out.println("克隆後s2:name=" + s2.p.name + "," + "age=" + s2.p.age);
		}
	}
	class Professor implements Cloneable {
		String name;
		int age;

		Professor(String name, int age) {
			this.name = name;
			this.age = age;
		}
		public Object clone() {
			Object o = null;
			try {
				o = super.clone();
			} catch (CloneNotSupportedException e) {
				System.out.println(e.toString());
			}
			return o;
		}
	}

克隆後s1:name=wangwu,age=50
克隆後s2:name=wangwu,age=50
克隆後s1:name=wangwu,age=50
克隆後s2:name=lisi,age=30

修改s2的教授不會影響s1的教授。
因此,在使用clone時,一定要分清需要克隆的物件屬性。


10、替代方法:
另一個實現物件拷貝的好辦法是提供一個拷貝構造器(copy constructor)或拷貝工廠(copy factory)。
拷貝構造器只是一個構造器,它唯一的引數型別是包含該構造器的類,例如:
public Yum(Yum yum);
    拷貝工廠是類似於拷貝構造器的靜態工廠:
public static Yum newInstance(Yun yum);
    拷貝構造器的做法,以及靜態工廠方法的變形,都比Cloneable/clone方法具有更多的優勢:
它們不依賴域某一種很有風險的,語言之外的物件建立機制;
它們不要求遵守尚未定製好的文件規範,不會與final域的正常使用發生衝突,它們不會丟擲不必要的受檢異常,不需要進行型別轉換。
    更進一步,拷貝構造器或者拷貝工廠可以帶一個引數,引數型別是通過該類實現的介面。
例如,按照管理,所有通用集合實現都提供了一個拷貝構造器,它的引數型別為Collection或者是Map。
基於介面的拷貝構造器和拷貝工廠,能允許使用者選擇拷貝的實現型別,而不是強迫客戶接受原始的實現型別。
例如,現在有一個HashSet,並且希望把它拷貝成一個TreeSet。clone方法無法提供這樣的功能,但是用拷貝構造器很容易實現,new TreeSet(s)。


如果想要了解更多有關深拷貝和淺拷貝的資訊,可以去看設計模式原型模式那節,有非常清晰的介紹。


每天努力一點,每天都在進步。