23种设计模式4_原型模式
1 基本介绍
原型模式(Prototype模式):用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。该模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节。
原理:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝他们自己来实施创建,即对象.clone()
2 克隆羊问题
现在有一只羊,姓名:tom,年龄:1,颜色:白色。请编写程序创建和tom属性完全相同的100只羊。
3 传统方式实现
// 创建羊的类
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 + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
// 传统方式
Sheep tomSheep = new Sheep("tom", 1, "白色");
// 演示先创建5只羊
System.out.println(new Sheep(tomSheep.getName(), tomSheep.getAge(), tomSheep.getColor()));
System.out.println(new Sheep(tomSheep.getName(), tomSheep.getAge(), tomSheep.getColor()));
System.out.println(new Sheep(tomSheep.getName(), tomSheep.getAge(), tomSheep.getColor()));
System.out.println(new Sheep(tomSheep.getName(), tomSheep.getAge(), tomSheep.getColor()));
System.out.println(new Sheep(tomSheep.getName(), tomSheep.getAge(), tomSheep.getColor()));
}
}
分析:
①优点好理解,简单易操作
②在创建新的对象时,总是需要重新获取原始对象的属性,如果创建的对象比较复杂时,效率较低
③总是需要重新初始化对象,而不是动态地获得对象运行时状态,不够灵活
改进思路:java种Object类是所有类的跟类,Object类提供了一个clone()方法,该方法可以将一个java对象复制一份,但是需要实现clone的java类必须要实现接口Cloneable,该接口表示该类能够复制且有复制的能力,这就引申出原型模式。
4 通过原型模式实现
实现步骤:
①Prototype:声明一个原型类,即一个克隆自己的接口。
②ConcretePrototype:创建一个具体的圆形类,实现一个克隆自己的操作。
③CLient:让一个原型对象克隆自己,从而创建一个新的对象(属性)。
首先先对羊这个类进行下修改:
public class Sheep implements Cloneable {
private String name;
private int age;
private String color;
// 构造器不变,省略。。。
// get/set方法不变,省略。。。
// toString()也不变,省略。。。
// 克隆该实例,使用默认的克隆方法来完成
@Override
protected Object clone() throws CloneNotSupportedException {
return (Sheep) super.clone();
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
Sheep tomSheep = new Sheep("tom", 1, "白色");
// 通过原型模式演示先创建5只羊
System.out.println("sheep1 = " + tomSheep.clone());
System.out.println("sheep2 = " + tomSheep.clone());
System.out.println("sheep3 = " + tomSheep.clone());
System.out.println("sheep4 = " + tomSheep.clone());
System.out.println("sheep5 = " + tomSheep.clone());
}
}
演示结果如下:
5 实际应用场景演示
案例需求:现在需要给很多用户发送邮件,邮件格式:“标题:XXX公司春节抽奖活动 活动内容:1234567890(纯演示用了) 收件人:{邮件地址} …发送成功!”
// 邮件模板
public class MailTemplate {
// 标题
private String mailSubject = "标题:XXX公司春节抽奖活动";
// 邮件内容
private String mailContext = "1234567890";
public String getMailSubject() {
return this.mailSubject;
}
public String getMailContext() {
return this.mailContext;
}
}
// 邮件类
public class Mail {
// 收件人
private String receiver;
// 主题
private String mailSubject;
// 邮件内容
private String mailContext;
public Mail(MailTemplate mailTemplate) {
this.mailSubject = mailTemplate.getMailSubject();
this.mailContext = mailTemplate.getMailContext();
}
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getMailSubject() {
return mailSubject;
}
public void setMailSubject(String mailSubject) {
this.mailSubject = mailSubject;
}
public String getMailContext() {
return mailContext;
}
public void setMailContext(String mailContext) {
this.mailContext = mailContext;
}
}
public class Client {
public static void main(String[] args) {
// 这里定义假设发给10个人
int total = 10;
int count = 0;
MailTemplate mailTemplate = new MailTemplate();
Mail mail = new Mail(mailTemplate);
while (count < total) {
mail.setReceiver(generateReceiver());
sendMail(mail);
count++;
}
}
public static void sendMail(Mail mail) {
System.out.println(mail.getMailSubject() + "\t收件人:" + mail.getReceiver() + "\t活动内容:" + mail.getMailContext() + "\t" + "...发送成功!");
}
public static String randomString(int len) {
String source = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i=0; i<len; i++) {
sb.append(source.charAt(random.nextInt(source.length())));
}
return sb.toString();
}
public static String generateReceiver() {
return randomString(8) + "@" + randomString(3) + ".com";
}
}
从上述案例可以看到实现是很简单的,但是有一个问题,我们的这个是单线程的,那么就需要等待上一封邮件发送完毕才能发送下一封邮件,如果邮件数量够多,是不是非常的耗时。那么改成多线程的呢,多线程效率是提高了,但是如果前一封邮件还没有发出去,另一个线程的邮件又进来了,那不是出现问题了。这种情况下我们就可以通过原型模式来进行修改,如下:
// 在mail类实现Cloneable接口,并重写clone()方法
@Override
protected Mail clone() throws CloneNotSupportedException {
return (Mail) super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
// 这里定义假设发给10个人
int total = 10;
int count = 0;
Mail mail = new Mail(new MailTemplate());
while (count < total) {
Mail cloneMail = mail.clone();
cloneMail.setReceiver(generateReceiver());
sendMail(cloneMail);
count++;
}
}
实现效果是一样的。
分析:
①性能优良 原型模式实在内存二进制流中进行拷贝,要比直接new对象性能好很多,特别是在循环体内需要用到大量的对象时,原型模式可以更好的体现优先。
②逃避构造函数的约束 这一点既是优点也是缺点,直接在内存中进行拷贝,构造函数是不会执行的,所以需要根据实际试用场景选择使用。
6 使用场景和注意事项
使用场景
①资源优化 类初始化过程中需要消耗非常多的资源,包括数据、硬件资源等。
②性能和安全要求的场景 通过new方式创建对象需要准备繁琐的数据或者存在访问权限时,可以使用原型模式
③一个对象多个修改者的场景 对象提供给其他对象使用时,每个调用者都需要设置不同的属性,可以考虑该模式
注意事项
①clone()方法的原理时从内存中以二进制流的方式进行拷贝,重新分配一块内存区域,是不会执行构造函数的。
②对象的clone与对象内的final关键字是有冲突的。要使用clone方法,就不要在类的成员变量上增加final关键字。
7 深拷贝和浅拷贝
先看一个案例:
public class Thing implements Cloneable {
private ArrayList<String> list = new ArrayList<>();
@Override
public Thing clone() throws CloneNotSupportedException {
return (Thing) super.clone();
}
public void setValue(String str) {
this.list.add(str);
}
public ArrayList<String> getValue() {
return this.list;
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建一个对象,并设置一个值
Thing thing = new Thing();
thing.setValue("zhangsan");
// 拷贝对象
Thing cloneThing = thing.clone();
cloneThing.setValue("lisi");
System.out.println(thing.getValue());
}
}
运行结果如下:
思考一下,怎么会有lisi呢?是因为java做了一个偷懒的拷贝动作,Object类提供的clone方法只拷贝对象本身,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝。两个对象公用一个私有变量,你变我也变,非常不安全。那么为什么上面演示的Mail案例中没有问题呢?因为对于基础数据类型,比如int,long,char等都会被拷贝的,String类型比较特殊,它是存储在字符串池中的,当需要使用的时候,就会在内存中创建新的字符串。
注意 使用原型模式时,引用的成员变量必须满足两个条件才不会被拷贝:一是类的成员变量,而不是方法变量;二是必须是一个可变的引用对象,而不是一个原始类型或者不可变对象。
浅拷贝是有风险的,对上述浅拷贝的案例进行优化:
public class Thing implements Cloneable {
private ArrayList<String> list = new ArrayList<>();
@Override
public Thing clone() throws CloneNotSupportedException {
Thing cloneThing = (Thing) super.clone();
// 添加对私有变量进行独立的拷贝
cloneThing.setList((ArrayList<String>) this.list.clone());
return cloneThing;
}
public void setValue(String str) {
this.list.add(str);
}
public ArrayList<String> getValue() {
return this.list;
}
public ArrayList<String> getList() {
return list;
}
public void setList(ArrayList<String> list) {
this.list = list;
}
}
运行结果如下:
这样就实现了完全拷贝,两个对象之间就没有关系了,你改你的,我改我的,互不影响,这种拷贝就是深拷贝。
深拷贝还有一种是实现方式就是自己写二进制流来操作对象,然后实现对象的深拷贝。
注意 深拷贝和浅拷贝建议不要混合使用,特别是在涉及类的继承时,父类有多个引用的情况下就非常复杂,建议深拷贝和浅拷贝分开来实现。