克隆羊多利之原型模式
今天就不想開頭了,直接來吧。如果想要創建幾個和某對象一模一樣的新對象,我們很容易想到new對象,在構造器裡面進行複製即可。但是今天就看一個新模式,原型模式。
創建對象的正常方法(蠢方法)
背景
新建一個Sheep類,並在客戶端Client裡面創建一個sheep對象,那我們還想要多添加幾個克隆羊,即和sheep對象一模一樣的幾個對象。我們很容易想到的是,直接new方法,再通過構造方法,將sheep裡面的幾個參數進行賦值,具體程式碼如下。
程式碼
Sheep類:
public class Sheep { private String name; private int age; private String color; public Sheep(String name, int age, String color) { this.name = name; this.age = age; this.color = color; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } @Override public String toString() { return "Sheep{" + "name='" + name + '\'' + ", age=" + age + ", color='" + color + '\'' + '}'; } }
Client類:
public class Client { public static void main(String[] args) throws CloneNotSupportedException { Sheep sheep = new Sheep("tom", 1, "白色"); Sheep sheep1=new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor()); Sheep sheep2=new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor()); System.out.println(sheep1.toString()); System.out.println(sheep2.toString()); System.out.println(sheep.hashCode()+","+sheep1.hashCode()+","+sheep2.hashCode()); } }
運行結果:
優點
比較好理解,簡單易操作,傻白甜操作。
缺點
1.在創建新的對象時,總是需要獲取原始對象的值,如果創建的對象比較複雜,效率較低。
2.總是需要重新初始化對象,而不是動態的獲取對象運行時的狀態,不夠靈活
引入原型模式
官方概念
用原型實例(原來的對象)來指定創建對象的種類,並且通過拷貝這些原型,創建新的對象。
白話文
就是通過一個舊對象,創建一個新對象,即克隆羊多利。打個比方,咱能單個複製多利身上的部位,然後拼起來,但是這種方法麻煩且低效,部位的增加可能導致越來越低能。我們也能通過某種方法,直接複製所有。某種方法映射到設計模式裡面就是原型模式。
創建對象的新方法(clone)
方法概念描述
原型類需要具備以下兩個條件
:
- 實現Cloneable介面。在java語言有一個Cloneable介面,它的作用只有一個,就是在運行時通知虛擬機可以安全地在實現了此介面的類上使用clone方法。
- 重寫Object類中的clone方法。Java中,所有類的父類都是Object類,Object類中有一個clone方法,作用是返回對象的一個拷貝,但是其作用域protected類型的,一般的類無法調用,因此,原型類需要將clone方法的作用域修改為public類型。
程式碼
Sheep類:(與之前的Sheep區別就是實現了Cloneable介面及重寫了clone方法)
public class Sheep implements Cloneable { private String name; private int age; private String color; public Sheep(String name, int age, String color) { this.name = name; this.age = age; this.color = color; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } @Override public String toString() { return "Sheep{" + "name='" + name + '\'' + ", age=" + age + ", color='" + color + '\'' + '}'; } @Override protected Object clone() throws CloneNotSupportedException { Sheep otherSheep=(Sheep)super.clone(); return otherSheep; } }
Client類:
public class Client { public static void main(String[] args) throws CloneNotSupportedException { Sheep sheep = new Sheep("tom", 1, "白色"); Sheep sheep1 = (Sheep) sheep.clone(); System.out.println(sheep.toString()); System.out.println(sheep1.toString()); System.out.println(sheep.hashCode()+","+sheep1.hashCode()); } }
運行結果:
插曲(為什麼要實現Cloneable介面,可不可以不實現?)
在上面的方法描述中我們可以知道了要實現Cloneable介面,但是我們看看如果不實現,有什麼問題?
Sheep類:
public class Sheep{ private String name; private int age; private String color; //構造器,setter/getter,toString就先略去了,看重點 @Override protected Object clone() { Sheep otherSheep=null; try{ otherSheep=(Sheep)super.clone(); return otherSheep; }catch(Exception e){ e.printStackTrace(); } return otherSheep; } }
test類一樣,我就不貼了,直接來看運行結果:
我們可以看到直接拋出了異常,這是因為如果對象的類不支援Cloneable介面,重寫clone方法的子類也可以引發此異常以指示無法克隆實例。所以啊,都要寫,不要偷懶。
淺拷貝
舉個例子,如果羊對象裡面有個house這種的複雜對象,我們看能不能拷貝成功。
Sheep類:
public class Sheep implements Cloneable{ private String name; private int age; private String color; private House house; public Sheep(String name, int age, String color,House house) { this.name = name; this.age = age; this.color = color; this.house=house; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public House getHouse() { return house; } public void setHouse(House house) { this.house = house; } @Override public String toString() { return "Sheep{" + "name='" + name + '\'' + ", age=" + age + ", color='" + color + '\'' + ", house=" + house + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
House類:
public class House implements Cloneable { private String name; private String address; public House(String name, String address) { this.name = name; this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
Client類:
public class Client { public static void main(String[] args) throws CloneNotSupportedException { House house=new House("羊羊家","地球上"); Sheep sheep = new Sheep("tom", 1, "白色",house); Sheep sheep1 = (Sheep) sheep.clone(); System.out.println(sheep.getHouse().hashCode()+","+sheep1.getHouse().hashCode()); } }
運行結果:
我們可以看到兩個Sheep對象中house的hashCode並沒有變化,說明他們實際上指向的是同一片空間,即為淺拷貝,那我們之前的例子都是淺拷貝,因為Sheep對象中的各個屬性的hashCode是一樣的,他們指向的是同一片記憶體空間。那我們應該如何讓裡面對象的值也不一樣呢?那就是下面的深拷貝。
深拷貝(複雜對象)
方法一(使用自己子對象的clone方法)
Sheep類:(與之前的區別只是方法上的區別,所以就不貼重複程式碼啦)
@Override protected Object clone() throws CloneNotSupportedException { Sheep otherSheep=(Sheep) super.clone(); otherSheep.setHouse((House)house.clone()); return otherSheep; }
方法二(序列化和反序列化)
public class Sheep implements Serializable, Cloneable { private String name; private int age; private String color; private House house; //setter/getter方法,構造器,toString方法都略去了 public Object clone2() { Sheep sheep = null; ByteArrayOutputStream bos = null; ObjectOutputStream oos = null; ByteArrayInputStream bis = null; ObjectInputStream ois = null; try { //序列化 bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos); oos.writeObject(this); bis = new ByteArrayInputStream(bos.toByteArray()); ois = new ObjectInputStream(bis); sheep = (Sheep) ois.readObject(); } catch (Exception e) { e.printStackTrace(); } finally { //關閉流 try { bos.close(); oos.close(); bis.close(); ois.close(); } catch (Exception e) { e.printStackTrace(); } } return sheep; } }
public class House implements Serializable,Cloneable { private String name; private String address; //setter/getter方法,構造器,toString方法都略去了 }
優點
在記憶體中二進位流的拷貝,比直接new一個對象的性能要好,畢竟人家是native方法,可以直接操作記憶體,是親兒子。
缺點
沒錯,親兒子也有缺點。當處理簡單類的時候,他的性能還不如new,但當處理複雜類的時候,性能就比new高出一大截啦。
咱來試試,普通的House類,有兩個基本類型的參數,分別是name和address,我們看new和利用原型模式分別用了多少毫秒?
public class House implements Serializable,Cloneable { private String name; private String address; public House(String name, String address) { this.name = name; this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
public class Client { public static void main(String[] args) throws CloneNotSupportedException { long startTime=System.currentTimeMillis(); for(int i=0;i<100000000;i++){ new House("羊羊家","地球上"); } System.out.println(System.currentTimeMillis()-startTime); startTime=System.currentTimeMillis(); House house=new House("羊羊家","地球上"); for(int i=0;i<100000000;i++){ house.clone(); } System.out.println(System.currentTimeMillis()- startTime); } }
運行結果:
從上圖我們可以看到採用new方法,只用了13毫秒,而採用原型模式,卻花了接近900毫秒,但是將House類的構造方法做一點點改變,我們就能發現差別,如下:
public class House implements Serializable,Cloneable { private String name; private String address; public House(String name, String address) { this.name = name.substring(1); this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
運行結果:
從上述程式碼可以看到,採用原型模式的時間比new方法的時間少了一半左右。
結語
原型模式屬於創建型模式,能產生新的對象。
其對複製功能進行優化,採用Java提供的native方法clone,前提是實現cloneable介面。
使用默認的clone方法,是淺拷貝,只是複製值,實際上地址並沒有變化。如果想要深拷貝,可以重寫clone方法或採用序列化和反序列化。