**
设计模式(三)之原型模式
**
-
案例说明
本文以获取多个属性相同但hashCode不同的市场实体类(内含水果实体类的引用类型属性)的实例对象的例子来体现原型模式的写法和优缺点,并对深拷贝和浅拷贝进行区分,结合UML类图和代码一起说明。
-
不用工厂模式的传统写法
我们在创建一个市场对象之后,想要再创建一个属性相同的市场对象,但两个对象所指向的内存地址不一样,传统写法可以直接new一个对象,并把原对象的属性set到新对象中去。
市场实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Market {
private String name;
private String address;
}
客户端
public class Client {
/**
* 模拟客户端
*/
public static void main(String[] args) {
// 原对象
Market market = new Market("东方市场", "东方一路");
// 新对象
Market market2 = new Market(market.getName(), market.getAddress());
System.out.println("market=" + market + " market2=" + market2);
System.out.println(market == market2);
}
}
测试结果显而易见,属性相同且内存地址不同,传统写法是可以满足我们的需求的。
D:\jdk8\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=54665:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath D:\jdk8\jre\lib\charsets.jar;D:\jdk8\jre\lib\deploy.jar;D:\jdk8\jre\lib\ext\access-bridge-64.jar;D:\jdk8\jre\lib\ext\cldrdata.jar;D:\jdk8\jre\lib\ext\dnsns.jar;D:\jdk8\jre\lib\ext\jaccess.jar;D:\jdk8\jre\lib\ext\jfxrt.jar;D:\jdk8\jre\lib\ext\localedata.jar;D:\jdk8\jre\lib\ext\nashorn.jar;D:\jdk8\jre\lib\ext\sunec.jar;D:\jdk8\jre\lib\ext\sunjce_provider.jar;D:\jdk8\jre\lib\ext\sunmscapi.jar;D:\jdk8\jre\lib\ext\sunpkcs11.jar;D:\jdk8\jre\lib\ext\zipfs.jar;D:\jdk8\jre\lib\javaws.jar;D:\jdk8\jre\lib\jce.jar;D:\jdk8\jre\lib\jfr.jar;D:\jdk8\jre\lib\jfxswt.jar;D:\jdk8\jre\lib\jsse.jar;D:\jdk8\jre\lib\management-agent.jar;D:\jdk8\jre\lib\plugin.jar;D:\jdk8\jre\lib\resources.jar;D:\jdk8\jre\lib\rt.jar;D:\ideaworkspace\design_pattern\design\target\classes;D:\dev_tools\repository\org\projectlombok\lombok\1.16.10\lombok-1.16.10.jar com.wd.prototype.none.Client
market=Market(name=东方市场, address=东方一路) market2=Market(name=东方市场, address=东方一路)
false
Process finished with exit code 0
- 一般的原型模式
我们让Market市场类实现Clonable接口,并重写clone方法。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Market implements Cloneable{
private String name;
private String address;
@Override
protected Object clone(){
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
客户端直接调用clone方法即可创建新的对象。
public class Client {
/**
* 模拟客户端
*/
public static void main(String[] args){
// 原对象
Market market = new Market("东方市场", "东方一路");
// 新对象
Market market2 = (Market)market.clone();
System.out.println("market=" + market + " market2=" + market2);
System.out.println(market == market2);
}
}
测试结果也满足属性相同,内存地址不同的需求。
D:\jdk8\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=54944:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath D:\jdk8\jre\lib\charsets.jar;D:\jdk8\jre\lib\deploy.jar;D:\jdk8\jre\lib\ext\access-bridge-64.jar;D:\jdk8\jre\lib\ext\cldrdata.jar;D:\jdk8\jre\lib\ext\dnsns.jar;D:\jdk8\jre\lib\ext\jaccess.jar;D:\jdk8\jre\lib\ext\jfxrt.jar;D:\jdk8\jre\lib\ext\localedata.jar;D:\jdk8\jre\lib\ext\nashorn.jar;D:\jdk8\jre\lib\ext\sunec.jar;D:\jdk8\jre\lib\ext\sunjce_provider.jar;D:\jdk8\jre\lib\ext\sunmscapi.jar;D:\jdk8\jre\lib\ext\sunpkcs11.jar;D:\jdk8\jre\lib\ext\zipfs.jar;D:\jdk8\jre\lib\javaws.jar;D:\jdk8\jre\lib\jce.jar;D:\jdk8\jre\lib\jfr.jar;D:\jdk8\jre\lib\jfxswt.jar;D:\jdk8\jre\lib\jsse.jar;D:\jdk8\jre\lib\management-agent.jar;D:\jdk8\jre\lib\plugin.jar;D:\jdk8\jre\lib\resources.jar;D:\jdk8\jre\lib\rt.jar;D:\ideaworkspace\design_pattern\design\target\classes;D:\dev_tools\repository\org\projectlombok\lombok\1.16.10\lombok-1.16.10.jar com.wd.prototype.normal.Client
market=Market(name=东方市场, address=东方一路) market2=Market(name=东方市场, address=东方一路)
false
Process finished with exit code 0
- 一般的原型模式,类中属性有引用数据类型。
市场类中新增了水果类的属性。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Market implements Cloneable{
private String name;
private String address;
private Fruit fruit;
@Override
protected Object clone(){
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
水果类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Fruit implements Cloneable{
private String name;
private String color;
@Override
protected Object clone(){
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
客户端新增了水果的属性以及水果属性的内存地址比较。
public class Client {
/**
* 模拟客户端
*/
public static void main(String[] args){
// 原对象
Market market = new Market("东方市场", "东方一路",new Fruit("苹果","红色"));
// 新对象
Market market2 = (Market)market.clone();
System.out.println("market=" + market + " market2=" + market2);
System.out.println(market == market2);
System.out.println(market.getFruit() == market2.getFruit());
}
}
测试发现,市场对象内存地址不一致而水果属性的内存地址一致,说明市场对象在调用clone方法时只是浅拷贝,属性中的引用数据表类型并没有另外开辟内存空间来拷贝,只是单纯拷贝了内存地址的指向位置。
D:\jdk8\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=55072:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath D:\jdk8\jre\lib\charsets.jar;D:\jdk8\jre\lib\deploy.jar;D:\jdk8\jre\lib\ext\access-bridge-64.jar;D:\jdk8\jre\lib\ext\cldrdata.jar;D:\jdk8\jre\lib\ext\dnsns.jar;D:\jdk8\jre\lib\ext\jaccess.jar;D:\jdk8\jre\lib\ext\jfxrt.jar;D:\jdk8\jre\lib\ext\localedata.jar;D:\jdk8\jre\lib\ext\nashorn.jar;D:\jdk8\jre\lib\ext\sunec.jar;D:\jdk8\jre\lib\ext\sunjce_provider.jar;D:\jdk8\jre\lib\ext\sunmscapi.jar;D:\jdk8\jre\lib\ext\sunpkcs11.jar;D:\jdk8\jre\lib\ext\zipfs.jar;D:\jdk8\jre\lib\javaws.jar;D:\jdk8\jre\lib\jce.jar;D:\jdk8\jre\lib\jfr.jar;D:\jdk8\jre\lib\jfxswt.jar;D:\jdk8\jre\lib\jsse.jar;D:\jdk8\jre\lib\management-agent.jar;D:\jdk8\jre\lib\plugin.jar;D:\jdk8\jre\lib\resources.jar;D:\jdk8\jre\lib\rt.jar;D:\ideaworkspace\design_pattern\design\target\classes;D:\dev_tools\repository\org\projectlombok\lombok\1.16.10\lombok-1.16.10.jar com.wd.prototype.normal2.Client
market=Market(name=东方市场, address=东方一路, fruit=Fruit(name=苹果, color=红色)) market2=Market(name=东方市场, address=东方一路, fruit=Fruit(name=苹果, color=红色))
false
true
Process finished with exit code 0
在这里简单提一下浅拷贝和深拷贝:
1、浅拷贝:
1)对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将
该属性值复制一份给新的对象;
2) 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类
的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内
存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个
实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成
员变量值;
3) 浅拷贝是使用默认的 clone()方法来实现。
2、深拷贝:
1) 复制对象的所有基本数据类型的成员变量值;
2) 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象里面的所有数据类型的属性进行拷贝;
3) 深拷贝实现方式1:重写clone方法来实现深拷贝;
4) 深拷贝实现方式2:通过对象序列化实现深拷贝(推荐)。
-
实现了深拷贝的原型模式,类中属性有引用数据类型。
我们将Market市场类中的clone方法加上一句,克隆引用类型Fruit即可实现。类图跟上面一样:
改动的代码如下:
@Override
protected Object clone() throws CloneNotSupportedException {
Market market = (Market)super.clone();
market.fruit = (Fruit)market.fruit.clone();
return market;
}
测试发现,水果实例也分配了不同的内存空间:
D:\jdk8\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=58284:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath D:\jdk8\jre\lib\charsets.jar;D:\jdk8\jre\lib\deploy.jar;D:\jdk8\jre\lib\ext\access-bridge-64.jar;D:\jdk8\jre\lib\ext\cldrdata.jar;D:\jdk8\jre\lib\ext\dnsns.jar;D:\jdk8\jre\lib\ext\jaccess.jar;D:\jdk8\jre\lib\ext\jfxrt.jar;D:\jdk8\jre\lib\ext\localedata.jar;D:\jdk8\jre\lib\ext\nashorn.jar;D:\jdk8\jre\lib\ext\sunec.jar;D:\jdk8\jre\lib\ext\sunjce_provider.jar;D:\jdk8\jre\lib\ext\sunmscapi.jar;D:\jdk8\jre\lib\ext\sunpkcs11.jar;D:\jdk8\jre\lib\ext\zipfs.jar;D:\jdk8\jre\lib\javaws.jar;D:\jdk8\jre\lib\jce.jar;D:\jdk8\jre\lib\jfr.jar;D:\jdk8\jre\lib\jfxswt.jar;D:\jdk8\jre\lib\jsse.jar;D:\jdk8\jre\lib\management-agent.jar;D:\jdk8\jre\lib\plugin.jar;D:\jdk8\jre\lib\resources.jar;D:\jdk8\jre\lib\rt.jar;D:\ideaworkspace\design_pattern\design\target\classes;D:\dev_tools\repository\org\projectlombok\lombok\1.16.10\lombok-1.16.10.jar com.wd.prototype.deep.Client
market=Market(name=东方市场, address=东方一路, fruit=Fruit(name=苹果, color=红色)) market2=Market(name=东方市场, address=东方一路, fruit=Fruit(name=苹果, color=红色))
false
false
Process finished with exit code 0
有朋友会问,如果引用类型的属性很多,那clone方法写起来也太麻烦和累赘了,那我们也可以用序列化和反序列化的方式来实现,让Market实现Serializable,添加一个deepClone方法:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Market implements Cloneable,Serializable{
private static final long serialVersionUID = 1L;
private String name;
private String address;
private Fruit fruit;
@Override
protected Object clone() throws CloneNotSupportedException {
Market market = (Market)super.clone();
market.fruit = (Fruit)market.fruit.clone();
return market;
}
public Market deepClone(){
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);
Market market = (Market)ois.readObject();
return market;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端
public class Client {
/**
* 模拟客户端
*/
public static void main(String[] args) throws CloneNotSupportedException {
// 原对象
Market market = new Market("东方市场", "东方一路",new Fruit("苹果","红色"));
// 新对象
Market market2 = (Market)market.deepClone();
System.out.println("market=" + market + " market2=" + market2);
System.out.println(market == market2);
System.out.println(market.getFruit() == market2.getFruit());
}
}
测试可看出,序列化反序列化的方法也能实现:
D:\jdk8\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=58518:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath D:\jdk8\jre\lib\charsets.jar;D:\jdk8\jre\lib\deploy.jar;D:\jdk8\jre\lib\ext\access-bridge-64.jar;D:\jdk8\jre\lib\ext\cldrdata.jar;D:\jdk8\jre\lib\ext\dnsns.jar;D:\jdk8\jre\lib\ext\jaccess.jar;D:\jdk8\jre\lib\ext\jfxrt.jar;D:\jdk8\jre\lib\ext\localedata.jar;D:\jdk8\jre\lib\ext\nashorn.jar;D:\jdk8\jre\lib\ext\sunec.jar;D:\jdk8\jre\lib\ext\sunjce_provider.jar;D:\jdk8\jre\lib\ext\sunmscapi.jar;D:\jdk8\jre\lib\ext\sunpkcs11.jar;D:\jdk8\jre\lib\ext\zipfs.jar;D:\jdk8\jre\lib\javaws.jar;D:\jdk8\jre\lib\jce.jar;D:\jdk8\jre\lib\jfr.jar;D:\jdk8\jre\lib\jfxswt.jar;D:\jdk8\jre\lib\jsse.jar;D:\jdk8\jre\lib\management-agent.jar;D:\jdk8\jre\lib\plugin.jar;D:\jdk8\jre\lib\resources.jar;D:\jdk8\jre\lib\rt.jar;D:\ideaworkspace\design_pattern\design\target\classes;D:\dev_tools\repository\org\projectlombok\lombok\1.16.10\lombok-1.16.10.jar com.wd.prototype.deep.Client
market=Market(name=东方市场, address=东方一路, fruit=Fruit(name=苹果, color=红色)) market2=Market(name=东方市场, address=东方一路, fruit=Fruit(name=苹果, color=红色))
false
false
Process finished with exit code 0
- 总结
1) 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率;
2) 不用重新初始化对象,而是动态地获得对象运行时的状态;
3) 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码;
4) 在实现深拷贝的时候可能需要比较复杂的代码;
5) 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了ocp原则。