原型模式
假设你现在是招商银行的客户经理,现在推出了一个信用卡消费抽奖的活动。一共有500万客户,你需要通过一个电子账单系统将客户的账单发送到各自的手中。因为银行发邮件都是有一定的要求的,首先开头肯定是“招商银行信用卡七一消费抽奖”等等,然后后面就是一些定制的信息,比如说周女士,刘先生等等,结尾就是什么版权啥的,大概就是这个样子的。
既然已经大概明白了这是一个啥样的问题,就开整!首先肯定需要一个模板,然后再从数据库中取出客户的基本信息,账单什么的,放到模板中形成一封完整的邮件发送给用户。类图如下图所示。
在上图中,AdvTemplate就是邮件的模板,Mail就是需要发送的邮件类,写成代码就是下面的代码。
/**
* @filename: AdvTemplate
* @description: 模板类
* @author: 撸代码的奥特曼
* @data:2021/7/2
*/
public class AdvTemplate {
//广告信名称
private String advSubject ="招商银行国庆信用卡抽奖活动";
//广告信内容
private String advContext = "国庆抽奖活动通知:只要刷卡就送你一百万!...";
//取得广告信的名称
public String getAdvSubject(){
return this.advSubject;
}
//取得广告信的内容
public String getAdvContext(){
return this.advContext;
}
}
/**
* @filename: Mail
* @description: 生成的邮件类
* @author: 撸代码的奥特曼
* @data:2021/7/2
*/
public class Mail {
//收件人
private String receiver;
//邮件名称
private String subject;
//称谓
private String appellation;
//邮件内容
private String contxt;
//邮件的尾部,一般都是加上"XXX版权所有"等信息
private String tail;
//构造方法
public Mail(AdvTemplate advTemplate){
this.contxt = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
//以下为getter/setter方法
........
}
/**
* @filename: Client
* @description: 发送邮件
* @author: 撸代码的奥特曼
* @data:2021/7/2
*/
public class Client {
//发送账单的数量,这个值是从数据库中获得
private static int MAX_COUNT = 6;
public static void main(String[] args) {
//模拟发送邮件
int i=0;
//把模板定义出来,这个是从数据库中获得
Mail mail = new Mail(new AdvTemplate());
mail.setTail("XX银行版权所有");
while(i<MAX_COUNT){
//以下是每封邮件不同的地方
mail.setAppellation(getRandString(5)+" 先生(女士)");
mail.setReceiver(getRandString(5)+"@"+getRandString(8) +".com");
//然后发送邮件
sendMail(mail);
i++;
}
}
//发送邮件
public static void sendMail(Mail mail){
System.out.println("标题:"+mail.getSubject() + "\n\t尊敬的: "+mail.getAppellation() + ":\n\t\t"
+mail.getContxt() + "\n\t\t\t\t\t" + mail.getTail());
}
//获得指定长度的随机字符串
public static String getRandString(int maxLength){
String source ="abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuffer sb = new StringBuffer();
SecureRandom rand = new SecureRandom();
for(int i=0;i<maxLength;i++){
sb.append(source.charAt(rand.nextInt(source.length())));
}
return sb.toString();
}
}
结果就是这个样子的...
标题:招商银行国庆信用卡抽奖活动
尊敬的: iAeui 先生(女士):
国庆抽奖活动通知:只要刷卡就送你一百万!...
XX银行版权所有
标题:招商银行国庆信用卡抽奖活动
尊敬的: yQLPi 先生(女士):
国庆抽奖活动通知:只要刷卡就送你一百万!...
XX银行版权所有
.............省略4个,代码跑起来见..................
现在你回头一看,业务类也写出来了,也能正常的发送到客户手中,好像没什么问题。但是再仔细想想,这是单线程的,如果按照一封邮件发送需要0.02秒(已经够小了,还要到数据库取数据出来呢)的时间的话,500万就是27.8小时,一天一夜都发不完!!!然后,你昨天的账单没发完呢,今天又有了新的账单,那可咋整???
这个时候你可能会想到使用多线程来解决,但是多线程还是会出现线程不安全的问题呀!因此,引入原型模式:通过对象的复制来解决这个问题。把上面那个类图稍微修改一丢丢,就得到了下面的这个类图。
看上面的类图,增加了一个Cloneable接口,这是Java自带的一个标记接口,只有实现这个接口后,然后在类中重写object的clone方法,然后通过类调用clone方法才能克隆成功,如果不实现这个接口,则会抛出CloneNotSupportedException(克隆不被支持)异常。修改之后的Mail类如下。
/**
* @filename: Mail
* @description: 生成的邮件类
* @author: 撸代码的奥特曼
* @data:2021/7/2
*/
public class Mail {
//收件人
private String receiver;
//邮件名称
private String subject;
//称谓
private String appellation;
//邮件内容
private String contxt;
//邮件的尾部,一般都是加上"XXX版权所有"等信息
private String tail;
//构造方法
public Mail(AdvTemplate advTemplate){
this.contxt = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
// 重写clone方法
@Override
public Mail clone(){
Mail mail =null;
try {
mail = (Mail)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return mail;
}
//以下为getter/setter方法
........
}
同时修改业务类代码。
public class Client {
//发送账单的数量,这个值是从数据库中获得
private static int MAX_COUNT = 6;
public static void main(String[] args) {
//模拟发送邮件
int i=0;
//把模板定义出来,这个是从数据库中获得
Mail mail = new Mail(new AdvTemplate());
mail.setTail("XX银行版权所有");
while(i<MAX_COUNT){
//以下是每封邮件不同的地方
Mail cloneMail = mail.clone();
cloneMail.setAppellation(getRandString(5)+"先生(女士)");
//然后发送邮件
sendMail(cloneMail);
i++;
}
}
//发送邮件
public static void sendMail(Mail mail){
System.out.println("标题:"+mail.getSubject() + "\n\t尊敬的: "+mail.getAppellation() + ":\n\t\t"
+mail.getContxt() + "\n\t\t\t\t\t" + mail.getTail());
}
//获得指定长度的随机字符串
public static String getRandString(int maxLength){
String source ="赵钱孙李周吴郑王";
StringBuffer sb = new StringBuffer();
SecureRandom rand = new SecureRandom();
for(int i=0;i<1;i++){
sb.append(source.charAt(rand.nextInt(source.length())));
}
return sb.toString();
}
}
和上面的进行比对可以知道,看Client类中的粗体字mail.clone()这个方法,把对象复制一份,产生一个新的 对象,和原有对象一样,然后再修改细节的数据,如设置称谓、设置收件人地址等。这种不通过new关键字来产生一个对象,而是通过对象复制来实现的模式就叫做原型模式。
定义
Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.(用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对 象。)原型模式的通用类图如下。
原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java 提供了一个Cloneable接口来标示这个对象是可拷贝的,为什么说是“标示”呢?翻开JDK的帮 助看看Cloneable是一个方法都没有的,这个接口只是一个标记作用,在JVM中具有这个标记 的对象才有可能被拷贝。那怎么才能从“有可能被拷贝”转换为“可以被拷贝”呢?方法是覆盖 clone()方法,是的,你没有看错是重写clone()方法。
@Override
public Mail clone(){}
原型模式的通用代码如下。
public class PrototypeClass implements Cloneable{
//覆写父类Object方法
@Override
public PrototypeClass clone(){
PrototypeClass prototypeClass = null;
try {
prototypeClass = (PrototypeClass)super.clone();
} catch (CloneNotSupportedException e) {
//异常处理
}
return prototypeClass;
}
}
优点:
-
性能优良 原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一 个循环体内产生大量的对象时,原型模式可以更好地体现其优点。
-
逃避构造函数的约束 这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的(参见下面的)。优点就是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。
注意事项
-
构造函数不会被执行
一个实现了Cloneable并重写了clone方法的类A,有一个无参构造或有参构造B,通过 new关键字产生了一个对象S,再然后通过S.clone()方式产生了一个新的对象T,那么在对象 拷贝时构造函数B是不会被执行的。
-
浅克隆和深克隆
先看一个例子。
/** * @filename: Student * @description: * @author: 撸代码的奥特曼 * @data:2021/7/5 */ public class Student{ private String name; //姓名 private int age; //年龄 private StringBuffer sex; //性别 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 StringBuffer getSex() { return sex; } public void setSex(StringBuffer sex) { this.sex = sex; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + ", sex=" + sex + "]"; } } /** * @filename: School * @description: * @author: 撸代码的奥特曼 * @data:2021/7/5 */ public class School implements Cloneable{ private String schoolName; //学校名称 private int stuNums; //学校人数 private Student stu; //一个学生 public String getSchoolName() { return schoolName; } public void setSchoolName(String schoolName) { this.schoolName = schoolName; } public int getStuNums() { return stuNums; } public void setStuNums(int stuNums) { this.stuNums = stuNums; } public Student getStu() { return stu; } public void setStu(Student stu) { this.stu = stu; } @Override public School clone(){ School school = null; try { school = (School) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return school; } @Override public String toString() { return "School{" + "schoolName='" + schoolName + '\'' + ", stuNums=" + stuNums + ", stu=" + stu + '}'; } } /** * @filename: ProrotypeTest * @description: 测试类 * @author: 撸代码的奥特曼 * @data:2021/7/5 */ public class ProrotypeTest { public static void main(String[] args) throws CloneNotSupportedException { School s1 = new School(); s1.setSchoolName("实验小学"); s1.setStuNums(100); Student stu1 = new Student(); stu1.setAge(20); stu1.setName("张三"); stu1.setSex(new StringBuffer("男")); s1.setStu(stu1); System.out.println("s1: "+s1+" s1的hashcode:"+s1.hashCode()+" s1中stu1的hashcode:"+s1.getStu().hashCode()); //调用重写的clone方法,clone出一个新的school---s2 School s2 = s1.clone(); System.out.println("s2: "+s2+" s2的hashcode:"+s2.hashCode()+" s2中stu1的hashcode:"+s2.getStu().hashCode()); } }
输出结果如下:
可以看到s1与s2的hashcode不同,也就是说clone方法并不是把s1的引用赋予s2,而是在堆中重新开辟了一块空间,将s1复制过去,将新的地址返回给s2。但是s1中stu的hashcode与s2中stu的hashcode相同,也就是这两个指向了同一个对象,修改s2中的stu会造成s1中stu数据的改变。但是修改s2中的基本数据类型与Stirng类型时,不会造成s1中数据的改变。这就是浅克隆: 复制对象时仅仅复制对象本身,包括基本属性,但该对象的属性引用其他对象时,该引用对象不会被复制,即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象是同一个。那怎么做才能实现s1和s2中的学生指向不同的地址呢?这就要涉及到深克隆的概念了--复制对象本身的同时,也复制对象包含的引用指向的对象,即修改被克隆对象的任何属性都不会影响到克隆出来的对象。首先需要让student类重写clone方法,实现cloneable接口。
@Override protected Student clone() throws CloneNotSupportedException { return (Student) super.clone(); }
然后,在school的clone方法中将school中的stu对象手动clone一下。
@Override
public School clone(){
School school = null;
try {
school = (School) super.clone();
school.stu = stu.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return school;
}
执行以下测试类结果如下:
这里可以看到两个stu的hashcode已经不同了,说明这已经是两个对象了。但是可以尝试着修改一下学生的sex属性。
Student stu2 = s2.getStu();stu2.setSex(stu2.getSex().append("45646"));s2.setStu(stu2);
结果如下:
可以看到当修改s2的学生的性别时,s1的也会发生变化,原因在于sex的类型是Stringbuffer,在clone的时候将StringBuffer对象的地址传递了过去,而StringBuffer类型没有实现cloneable接口,也没有重写clone方法。解决方法就是stu2.setSex(new StringBuffer("newString")); 在设置stu2的sex时创建一个新的StringBuffer对象。
上面的是通过实现clone方法来实现深克隆,还有另外一种方法就是通过序列化的方式实现深克隆。代码也比较简单,只需实现Serializable接口就好。
public class SerialSchool implements Serializable{
private String schoolName; //学校名称
private int stuNums; //学校人数
private SerialStudent stu; //一个学生
// getter and setter
}
public class SerialStudent implements Serializabl{
private String name; //姓名
private int age; //年龄
private StringBuffer sex; //性别
// getter and setter
}
/**
* @filename: SerialDeepCloneTest
* @description: 测试类
* @author: 撸代码的奥特曼
* @data:2021/7/5
*/
public class SerialDeepCloneTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerialSchool s1 = new SerialSchool();
s1.setSchoolName("实验小学");
s1.setStuNums(100);
SerialStudent stu1 = new SerialStudent();
stu1.setAge(20);
stu1.setName("张三");
stu1.setSex(new StringBuffer("男"));
s1.setStu(stu1);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(s1);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
SerialSchool s2 = (SerialSchool) ois.readObject();
SerialStudent cloneStudent = s2.getStu();
cloneStudent.setAge(18);
System.out.println("s1: "+s1+" s1的hashcode:"+s1.hashCode()+" s1中stu1的hashcode:"+s1.getStu().hashCode());
System.out.println("s2: "+s2+" s2的hashcode:"+s2.hashCode()+" s2中stu1的hashcode:"+s2.getStu().hashCode());
}
}