原型模式(Prototype)---创建型
1 基础知识
定义:原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。特征:不需要知道任何创建的细节,不调用构造方法。本质:克隆生成对象。
原型模式会要求对象实现一个可以“克隆”自身的接口,这样就可以通过拷贝或者是克隆一个实例对象本身来创建一个新的实例。如果把这个方法定义在接口上,看起来就像是通过接口来创建了新的接口对象。这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,也不关心它的具体实现,只要它实现了克隆自身的方法,就可以通过这个方法来获取新的对象,而无须再去通过new来创建。
几个概念的辨析:
(1)原型模式的功能
原型模式的功能实际上包含两个方面:一个是通过克隆来创建新的对象实例;另一个是为克隆出来的新的对象实例复制原型实例属性的值。原型模式要实现的主要功能就是:通过克隆来创建新的对象实例。一般来讲,新创建出来的实例的数据是和原型实例一样的。但是具体如何实现克隆,需要由程序自行实现,原型模式并没有统一的要求和实现算法。
(2)原型与new
原型模式从某种意义上说,就像是new操作,在前面的例子实现中,克隆方法就是使用new来实现的。但请注意,只是“类似于new”而不是“就是new”。克隆方法和new操作最明显的不同就在于:new一个对象实例,一般属性是没有值的,或者是只有默认值;如果是克隆得到的一个实例,通常属性是有值的,属性的值就是原型对象实例在克隆的时候,原型对象实例的属性的值。
(3)原型实例和克隆的实例
原型实例和克隆出来的实例,本质上是不同的实例,克隆完成后,它们之间是没有关联的,如果克隆完成后,克隆出来的实例的属性值发生了改变,是不会影响到原型实例的。
使用场景:
(1)类初始化消耗较多资源(2)new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)(3)构造函数比较复杂(4)循环体中生产大量的对象。
优点:性能比直接new要高、简化了创建过程
缺点:必须配备克隆方法、当进行复杂对象的克隆时要灵活运用深拷贝和浅拷贝,此时可能会引入风险。
2 代码示例
场景:发送许多封不同的邮件,并保存初始邮件的状态。
邮件类Mail:
/** * 邮件类 */ public class Mail { private String name; private String emailAddress; private String content; public Mail(){ System.out.println("Mail Class Constructor"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmailAddress() { return emailAddress; } public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return "Mail{" + "name='" + name + '\'' + ", emailAddress='" + emailAddress + '\'' + ", content='" + content + '\'' + '}'+super.toString(); } }
MailUtil类:
/** * 邮件工具类 */ public class MailUtil { //发送邮件 public static void sendMail(Mail mail){ String outputContent = "向{0}同学,邮件地址:{1},邮件内容:{2}发送邮件成功"; System.out.println(MessageFormat.format(outputContent,mail.getName(),mail.getEmailAddress(),mail.getContent())); } //保存邮件 public static void saveOriginMailRecord(Mail mail){ System.out.println("存储originMail记录,originMail:"+mail.getContent()); } }
Test类:
/** * 应用层调用 */ public class Test { public static void main(String[] args) { Mail mail = new Mail(); mail.setContent("初始化模板"); System.out.println("初始化mail:"+mail); for(int i = 0;i < 10;i++){ mail.setName("姓名"+i); mail.setEmailAddress("姓名"+i+"@imooc.com"); mail.setContent("恭喜您,此次慕课网活动中奖了"); MailUtil.sendMail(mail); } //保存最开始的邮件模板 MailUtil.saveOriginMailRecord(mail); } }
在调用应用层时发现,最后的保存邮件模板保存的是最后一次发送邮件的内容而不是一开始的模板。对于这个问题简单解决就是把这句代码位置放在紧挨着初始化模板后面,但在实际问题中有时还需要保存发送结果等情况,使得MailUtil.saveOriginMailRecord(mail);这句代码的位置不能随便移动那么便有如下代码解决方案:
public class Test { public static void main(String[] args) { Mail mail = new Mail(); mail.setContent("初始化模板"); System.out.println("初始化mail:"+mail); for(int i = 0;i < 10;i++){ Mail mailTemp = new Mail(); mailTemp.setName("姓名"+i); mailTemp.setEmailAddress("姓名"+i+"@imooc.com"); mailTemp.setContent("恭喜您,此次慕课网活动中奖了"); MailUtil.sendMail(mailTemp); } //保存最开始的邮件模板 MailUtil.saveOriginMailRecord(mail); } }
但如果Mail的构建十分复杂,那么for循环中不断的new的确不是一种性能良好的方法,此时便需要用到原型模式了。
对Mail类改造:实现Cloneable接口并重写方法。
public class Mail implements Cloneable{ private String name; private String emailAddress; private String content; public Mail(){ System.out.println("Mail Class Constructor"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmailAddress() { return emailAddress; } public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return "Mail{" + "name='" + name + '\'' + ", emailAddress='" + emailAddress + '\'' + ", content='" + content + '\'' + '}'+super.toString(); } @Override protected Object clone() throws CloneNotSupportedException { System.out.println("clone mail object"); return super.clone(); } }
在应用层:
public class Test { public static void main(String[] args) throws CloneNotSupportedException { Mail mail = new Mail(); mail.setContent("初始化模板"); System.out.println("初始化mail:"+mail); for(int i = 0;i < 10;i++){ //克隆时不调用构造器因此性能要优化很多 Mail mailTemp = (Mail) mail.clone(); mailTemp.setName("姓名"+i); mailTemp.setEmailAddress("姓名"+i+"@imooc.com"); mailTemp.setContent("恭喜您,此次慕课网活动中奖了"); //发送的是克隆的mailTemp MailUtil.sendMail(mailTemp); } //保存最初的mail MailUtil.saveOriginMailRecord(mail); } }
3 深拷贝与浅拷贝
Pig类:
public class Pig implements Cloneable{ private String name; private Date birthday; public Pig(String name, Date birthday) { this.name = name; this.birthday = birthday; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override public String toString() { return "Pig{" + "name='" + name + '\'' + ", birthday=" + birthday + '}'+super.toString(); } }
应用层:
public class Test { public static void main(String[] args) throws CloneNotSupportedException { Date birthday = new Date(0L); Pig pig1 = new Pig("佩奇",birthday); Pig pig2 = (Pig) pig1.clone(); //输出克隆后的对象 System.out.println(pig1); System.out.println(pig2); pig1.getBirthday().setTime(666666666666L); //输出pig1修改后的对象 System.out.println(pig1); System.out.println(pig2); } }
此时便出现了问题:克隆后更加toString()输出的字节码16进制数的确pig1 和 pig2 是不同的对象,但当pig1的生日变化后发现pig2也跟着变化,这是不应该发生的。debug后发现pig1和pig2的Date对象都是425因此一个变化另一个必然变化,这里就是发生了浅拷贝。
对Pig进行改造,对于引用的对象再进行一次单独的克隆
public class Pig implements Cloneable{ private String name; private Date birthday; public Pig(String name, Date birthday) { this.name = name; this.birthday = birthday; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override protected Object clone() throws CloneNotSupportedException { Pig pig = (Pig)super.clone(); //深拷贝,单独为生日实现克隆方法 pig.birthday = (Date) pig.birthday.clone(); return pig; } @Override public String toString() { return "Pig{" + "name='" + name + '\'' + ", birthday=" + birthday + '}'+super.toString(); } }
4 克隆破坏单例模式
先放着
5 源码中的使用
ArrayList类:
//在ArrayList中实现了Cloneable接口 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //重写clone方法 public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); //拷贝数组中的元素这里就避免了浅拷贝的发生,因为数组中可能存放的也是对象 v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }
同样还有在HashMap类中:
@Override public Object clone() { HashMap<K,V> result; try { //克隆对象 result = (HashMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } result.reinitialize(); //在克隆的对象存放元素 result.putMapEntries(this, false); return result; }
6 相关模式
(1)原型模式和抽象工厂模式
功能上有些相似,都是用来获取一个新的对象实例的。 不同之处在于,原型模式的着眼点是在如何创造出实例对象来,最后选择的方案是通过克隆;而抽象工厂模式的着眼点则在于如何来创造产品簇,至于具体如何创建出产品簇中的每个对象实例,抽象工厂模式则不是很关注。正是因为它们的关注点不一样,所以它们也可以配合使用,比如在抽象工厂模式里面,具体创建每一种产品的时候就可以使用该种产品的原型,也就是抽象工厂管产品簇,具体的每种产品怎么创建则可以选择原型模式。
(2)原型模式和生成器模式
这两种模式可以配合使用生成器模式关注的是构建的过程,而在构建的过程中,很可能需要某个部件的实例,那么很自然地就可以应用上原型模式,通过原型模式来得到部件的实例。
0