java設計模式---(4)原型模式
原型模式(Prototype Pattern)是用於建立重複的物件,同時又能保證效能。用原型例項來指定建立物件的種類,然後通過clone這個原型物件來建立新物件。
先定義一個學生類Student
他實現Cloneable
介面並重寫了clone()
方法(clone()方法是Object類中定義的一個方法,重寫clone()
方法要實現Cloneable `接口才行):
public class Student implements Cloneable { private String name; // 姓名 private String className; // 班級名稱 public Student(String name) { super(); this.setName(name); } @Override public Student clone() { Student student = null; try { student = (Student) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return student; } // get、set 、toString方法此處省不表 }
這個重寫的clone()方法也很簡單,就是將克隆的物件返回回來。
利用原型來建立一個新物件:
public class TestPrototype { public static void main(String[] args) { Student student = new Student("張三"); student.setClassName("一班"); Student clone = student.clone(); clone.setName("李四"); System.out.println(clone.toString()); // Student [name=李四, className=一班] } }
原型模式的根本就體現在這個clone()
方法上,所以要好好看看這個clone()
方法。clone()方法定義在Object類裡面:
首先,這個方法會丟擲CloneNotSupportedException異常,異常是在重寫clone()方法的類沒有直接或間接實現Cloneable介面的時候丟擲的,所以要重寫clone()方法時記得要實現Cloneable介面,順便說一下Cloneable介面是個空接口裡面什麼都沒定義,他就是起一個標識作用。
第二,通常來說對於任何物件,x.clone() != x這個判斷返回的是true(這是肯定的,因為克隆的物件是一個新物件)。並且x.clone().getClass() = = x.getClass() 這個判斷返回的是true,但是這個並不是絕對的。然後通常情況下x.clone().equals(x)這個表示式返回的也是true,當然這個也不是絕對的(言外之間就是通常情況下我們重寫了clone()方法的時候最好也重寫一下equals()方法
第三,按慣例,返回的物件應該遵守super.clone
這樣的方式來進行克隆,如果一個類或者他的父類遵守這個慣例,那麼克隆出來的物件就一定是相同型別的,一定滿足x.clone().getClass() == x.getClass() 這個判斷。
第四,按慣例,克隆方法返回的物件應該獨立於被克隆的物件。為了實現這種獨立性,在返回物件之前,可能需要修改super.clone方法返回物件的一個或多個欄位。通常來說,這意味著複製包含被克隆物件的內部“深層結構”的任何可變物件,並將對這些物件的引用替換為對副本的引用。如果一個類僅僅只包含基本型別(8種基本型別+string)的欄位或不可變物件的引用,那麼一般來說super.clone()返回的對旬都不用改變。這麼長一短話說白了意思就是:如果被克隆的物件中只包含(8種基本型別+string)或者一些不可變物件的引用,那麼clone()方法返回的結果物件中欄位值就跟被克隆的物件是一模一樣的,如果被克隆物件中包含有可變物件(像List、Map等等)那麼你在克隆的時候如果不管他,那克隆返回的物件就中是一個引用指向被克隆物件中的不可變物件,並不會去克隆一個新的可變物件。所以一般來說clone物件中如果包含有可變物件,那麼就要在clone()方法裡面再克隆一個這個可變物件。
第五,跟第一條說的一樣,如果沒有實現Cloneable介面就會丟擲CloneNotSupportedException 異常。注意,所有陣列都認為實現了Cloneable介面,並且陣列型別T[]的克隆方法的返回型別是T[],其中T是任何引用或基本資料型別。否則,此方法將建立此物件的類的新例項,並使用該物件的相應欄位的內容初始化其所有欄位,就像通過賦值一樣;欄位的內容本身不是克隆的。因此,此方法執行此物件的“淺拷貝”,而不是“深拷貝”操作。
第六,Object 物件本息並沒有實現Cloneable介面,所以Object物件上呼叫clone方法會丟擲一個執行時異常。
下面用程式碼來嘗試一下上面所說的6點:
Student
物件不實現Cloneable
介面,重寫clone()
方法:
public class Student {
private String name; // 姓名
@Override
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
public class TestPrototype {
public static void main(String[] args) {
Student student = new Student();
student.clone();
}
}
結果報錯:
java.lang.CloneNotSupportedException: com.qsx.pattern.prototype.entity.Student
at java.lang.Object.clone(Native Method)
Student
物件不實現Cloneable
介面,重寫clone()
方法和equals()
方法:
public class Student implements Cloneable {
private String name; // 姓名
@Override
public Student clone() { // 重寫克隆方法
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
@Override
public boolean equals(Object object) { // 重寫判斷相等方法
if (object instanceof Student) {
Student stu = (Student) object;
if (this.name == stu.name) {
return true;
}
}
return false;
}
// get set 方法省略
}
驗證:
public class TestPrototype {
public static void main(String[] args) {
Student student = new Student();
student.setName("aa");
System.out.println(student.clone() != student);
System.out.println(student.clone().getClass() == student.getClass());
System.out.println(student.clone().equals(student));
}
}
三個輸出都是true,第一個輸出克隆物件跟student物件本來就不是同一個物件所以肯定是true;第二個輸出根據上面第三條的說法,克隆方法按慣例這樣寫super.clone()
所以也是true;第三個輸出看到重寫的equals()
方法就知道這裡肯定是true了。
super.clone()
方法對於不同型別的欄位克隆的方式不一樣,如果是基本資料型別或者string或者陣列那麼clone就是像賦值一樣,這樣一來也就不用管其他操作了,直接clone就行了。但是對於那些物件中的所謂“可變物件”像List、Map之類的,那麼這個super.clone()
就只是將克隆的新物件中欄位的引到被克隆的物件中對應的欄位中去了。舉例子:
先定義克隆物件Student
他有2個“可變物件”作為屬性
public class Student implements Cloneable {
private String name; // 姓名
private List<String> parentNames; //可變物件List
private School school; // 可變物件bean
@Override
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
public void modifyParent(String name){ // 修改List
this.parentNames.add(name);
}
public void modifySchool(String name,String code){ // 修改bean物件
this.school.setCode(code);
this.school.setName(name);
}
// 省略get set 方法
}
檢驗結果:
public class TestPrototype {
public static void main(String[] args) {
List<String> pName = new ArrayList<>();
pName.add("father");
pName.add("mother");
School school = new School("小學","0123");
// new 個學生物件 並給2個可變物件賦值
Student s = new Student();
s.setParentNames(pName);
s.setSchool(school);
// 克隆學生物件
Student c = s.clone();
// 修改克隆物件的值
c.modifyParent("Adoptive father");
c.modifySchool("大學", "3210");
System.out.println("被克隆物件的ParentNames:"+s.getParentNames().toString());
System.out.println("被克隆物件的School:"+s.getSchool().toString());
System.out.println("克隆物件的ParentNames:"+c.getParentNames().toString());
System.out.println("克隆物件的School:"+c.getSchool().toString());
System.out.println(s.getSchool().hashCode());
System.out.println(c.getSchool().hashCode());
System.out.println(s.getParentNames().hashCode());
System.out.println(c.getParentNames().hashCode());
}
}
輸出結果:
被克隆物件的ParentNames:[father, mother, Adoptive father]
被克隆物件的School:School [name=大學, code=3210]
克隆物件的ParentNames:[father, mother, Adoptive father]
克隆物件的School:School [name=大學, code=3210]
705927765
705927765
-1506096756
-1506096756
根據結果我們可以看到,我們在修改克隆物件的parentNames或者school的時候被克隆物件也一樣改了,而下面的hashCode則進一步證明了,實際上克隆物件中的parentNames和school與被克隆物件中的這2個屬性是同一個。
這個問題怎麼解決呢?我們可以在Student
類的clone()
方法裡面手動的去克隆這2個“可變物件”,下面只寫clone()
方法的程式碼,其他程式碼都一樣:
@SuppressWarnings("unchecked")
@Override
public Student clone() {
Student student = null;
try {
student = (Student) super.clone(); // 克隆物件自己
student.school = (School) this.school.clone(); // 克隆物件的school屬性
student.parentNames = (List<String>) ((ArrayList<String>) this.parentNames).clone(); // 克隆物件的List屬性
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
測試程式碼不變,結果如下:
被克隆物件的ParentNames:[father, mother]
被克隆物件的School:School [name=小學, code=0123]
克隆物件的ParentNames:[father, mother, Adoptive father]
克隆物件的School:School [name=大學, code=3210]
705927765
366712642
-2144869208
-1506096756
可以看見修改克隆物件屬性值的時候並沒有修改到被克隆物件的值,而且克隆和被克隆物件中相同屬性的hashCode也不一樣了。
這裡要說明一下的是:Student
中克隆了school
屬性和parentNames
屬性,但是有個前提不能忘了,School
本身一定要去實現Cloneable
介面,另外就是parentNames
屬性的型別是List<T>
,List<T>
是個介面,所以我們只能去找List的介面的實現類中實現了Cloneable
介面的類來進行克隆(如這裡的ArrayList<T>
)。