定义
原型模式(Prototype Pattern)的定义如下:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
通俗理解
我们生活中经常会用到复印机,用来复印书籍、简历、身份证、银行卡... ...但是,有没有想过,为什么我们书籍、简历这些,要选择复印,而不是重新排版,打印一份呢?(在讨论一些理所当然的东西的时候,总会有一股哲学的味道)。你可以说,这很理所当然呀!重新排版还得消耗人力物力,反正书籍、简历这些都是一样的,直接复印一份不就行了么?
原型模式,就是复印简历的过程,简历都差不多,选择以前的简历进行复印,那么可以节省排版、打印的人力物力。在程序中也是一样,创建差不多的对象,我们可以通过new对象来创建,这样的一种方式会导致new出对象之后,还要对它们的属性进行setter,然后才能够使用。如果我们使用原型模式,对旧的对象进行复制,然后修改它们不同的属性,这一方面可以避免了大量的setter;另外一个方面是节约了资源,创建对象更快,更节省时间(《JVM——Java关键字背后的故事》会有详细的比较与原理,未写)。
示例
业务为复制简历。
渣渣程序
不按照原型模式来,我们可以写出这样的程序:
简历
public class CV {
/**
* 名字
*/
private String name;
/**
* 年龄
*/
private int age;
/**
* 技能
*/
private String skill;
/**
* 住址
*/
private Address address;
// setter和getter省略
}
住址
public class Address {
/**
* 国家
*/
private String country;
/**
* 省
*/
private String province;
/**
* 市
*/
private String city;
/**
* 街道
*/
private String street;
//setter和getter省略
}
调用方
public class Main {
public static void main(String[] args) {
//Jack的简历1
Address jackAddr1 = new Address();
jackAddr1.setCountry("China");
jackAddr1.setProvince("Guangdong");
jackAddr1.setCity("Shenzhen");
jackAddr1.setStreet("Baoan");
CV jack1 = new CV();
jack1.setName("Jack");
jack1.setAge(24);
jack1.setSkill("coding");
jack1.setAddress(jackAddr1);
//后来Jack搬家,搬到西乡
Address jackAddr2 = new Address();
jackAddr2.setCountry("China");
jackAddr2.setProvince("Guangdong");
jackAddr2.setCity("Shenzhen");
jackAddr2.setStreet("Xixiang");
CV jack2 = new CV();
jack2.setName("Jack");
jack2.setAge(24);
jack2.setSkill("coding");
jack2.setAddress(jackAddr2);
}
}
可怜🤕的Jack,因为家搬到西乡了,就要重新修改一份简历,真是费时费力。
优化
类图
通过这个类图,就可以知道,最关键的是clone方法。因此,原型模式,最关键的是写好clone方法。
程序
浅克隆
Java里面有一个声明式的接口Cloneable,这个接口本身没有写任何的方法,只是告诉JVM,该接口的实现类需要开放“克隆”功能,而且在Object当中,clone
是一个native
方法,表示是JVM直接提供的方法(《JVM——Java关键字背后的故事》会有详细的比较与原理,未写),我们实现这个接口,并重写Object的clone方法就可以了完成对象的拷贝了。
简历
public class CV implements Cloneable{
/**
* 名字
*/
private String name;
/**
* 年龄
*/
private int age;
/**
* 技能
*/
private String skill;
/**
* 住址
*/
private Address address;
/**
* 等级
*/
private Integer level;
// setter和getter省略... ...
public CV clone() {
Object cloneCV = null;
try {
cloneCV = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return (CV)cloneCV;
}
}
地址
public class Address{
/**
* 国家
*/
private String country;
/**
* 省
*/
private String province;
/**
* 市
*/
private String city;
/**
* 街道
*/
private String street;
}
调用方
public class Main {
public static void main(String[] args) {
//Jack的简历1
Address jackAddr1 = new Address();
jackAddr1.setCountry("China");
jackAddr1.setProvince("Guangdong");
jackAddr1.setCity("Shenzhen");
jackAddr1.setStreet("Baoan");
CV jack1 = new CV();
jack1.setName("Jack");
jack1.setAge(24);
jack1.setSkill("coding");
jack1.setLevel(1);
jack1.setAddress(jackAddr1);
//后来Jack搬家,搬到西乡
CV jack2 = jack1.clone();
jack2.getAddress().setStreet("Xixiang");
jack2.setAge(26);
jack2.setName("Mack");
jack2.setLevel(3);
System.out.println(jack1.getAddress().getStreet());//Xixiang
System.out.println(jack2.getAddress().getStreet());//Xixiang
System.out.println(jack1.getAge());//24
System.out.println(jack2.getAge());//26
System.out.println(jack1.getName());//Jack
System.out.println(jack2.getName());//Mack
System.out.println(jack1.getLevel());//1
System.out.println(jack2.getLevel());//3
}
}
通过上面的方式,就可以对对象进行拷贝。很不幸的是,super.clone()
只是对对象的浅克隆,只有基本类型(int,boolean
等)和String
会被复制新的一份,而引用类型(Integer,类,接口,数组等
),还是会指向原来的内存地址,造成Jack搬到Xixiang
后,第一份简历和第二份简历都被改成Xixiang
深克隆
如果我们也需要把引用类型的属性也复制一份,我们应该怎么改?
序列化,反序列化
public CV clone() {
CV cv = null;
try {
// 将对象写入到流
FileOutputStream fos = new FileOutputStream("clone.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(this);
// 从流中写入到对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("clone.out"));
cv = (CV) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return cv;
}
}
逐个属性克隆
public CV clone() {
CV cv = null;
try {
cv = (CV) super.clone();
cv.address = this.address.clone();
} catch (Exception e) {
e.printStackTrace();
}
return cv;
}
优点
- 直接在内存中创建对象,提高创建对象的效率
- 不使用构造器创建对象,减少了束缚
- 可以保存对象的状态,可应用于对象的撤销操作
缺点
- 需要克隆对象以及引用的类型都实现clone方法,否则用不了。如果引用嵌套比较深,那么就需要一层一层地去修改。
- 不使用构造器创建对象,减少了束缚
应用场景
- 资源优化,类的初始化需要很多资源的,包含数据和硬件资源
- 性能和安全要求比较高
- 一个对象多个修改者,拷贝出备份的让修改者去修改
注意事项
-
clone()
无需调用构造器就能创建对象 -
final
修饰的属性无法使用clone()
方法 - 最好别用