引言
回顾上一节我们讲的状态模式,这节我们来讲一下原型模式。和原型模式相关的2个概念:浅拷贝和深拷贝。
示例地址
Demo
先上类图
再看定义
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
使用场景
1. 类初始化需要消耗非常多的资源,这个资源包括数据、硬件资源等,通过原型拷贝避免这些消耗。
2. 通过new产生一个对象需要非常繁琐的数据准备或者访问权限,
3. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝。
注意事项
通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比通过new操作速度快,只有当通过new构造对象较为耗时或者说成本较高时,通过clone方法才能够获得效率上的提升。因此,在使用Cloneable时需要考虑构建对象的成本以及做一些效率上的测试。
浅拷贝
浅拷贝是将原始对象中的数据型字段拷贝到新对象中去,将引用型字段的“引用”复制到新对象中去,不把“引用的对象”复制进去,所以原始对象和新对象引用同一对象,新对象中的引用型字段发生变化会导致原始对象中的对应字段也发生变化。
1. 示例
/**
* 1.浅拷贝拷贝外层对象,对象里面的引用对象不进行拷贝。
* 2.深拷贝需要进行内部的拷贝(人为进行拷贝)。
* @author [email protected]
* @created 2018/7/13 下午4:10.
*/
public class Person implements Cloneable {
private String name;
private Son mSon;
public Person(String name, int age) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Son getSon() {
return mSon;
}
public void setSon(Son son) {
mSon = son;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", mSon=" + mSon +
'}';
}
}
/**
* @author [email protected]
* @created 2018/7/13 下午4:54.
*/
public class Son implements Cloneable {
public Son(String schoolName) {
this.schoolName = schoolName;
}
private String schoolName;
public String getSchoolName() {
return schoolName;
}
public void setSchoolName(String schoolName) {
this.schoolName = schoolName;
}
protected Son clone() {
Son clone = null;
try {
clone = (Son) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e); // won't happen
}
return clone;
}
@Override
public String toString() {
return "Son{" +
"schoolName='" + schoolName + '\'' +
'}';
}
}
2. 调用
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Person p = new Person("zhang");
Son son = new Son("实验二小");
p.setSon(son);
Person p1 = null;
try {
p1 = (Person) p.clone();
p1.setName("yang");
son.setSchoolName("希望小学");
System.out.println("----" + p.toString());
System.out.println("----" + p1.toString());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
3. 打印
System.out: ----Person{name='zhang', mSon=Son{schoolName='希望小学'}}
System.out: ----Person{name='yang', mSon=Son{schoolName='希望小学'}}
4. 分析原因
Object 类提供的方法 clone 只是拷贝本对象,
其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址。原始类型比如int,long,String(Java 就希望你把 String 认为是基本类型,String 是没有 clone 方法的)等都会被拷贝的。所以String改变了,引用的Son两者还是一样的,没有改变。
深拷贝
深拷贝是在引用方面不同,深拷贝就是创建一个新的和原始字段的内容相同的字段,是两个一样大的数据段,所以两者的引用是不同的,之后的新对象中的引用型字段发生改变,不会引起原始对象中的字段发生改变。
1. 示例
/**
* 深拷贝
*
* @author [email protected]
* @created 2018/7/13 下午4:53.
*/
public class PersonTwo implements Cloneable {
private String name;
private Son mSon;
public PersonTwo(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Son getSon() {
return mSon;
}
public void setSon(Son son) {
mSon = son;
}
@Override
protected Object clone() {
PersonTwo clone = null;
try {
clone = (PersonTwo) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e); // won't happen
}
clone.mSon = mSon.clone();
return clone;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", mSon=" + mSon +
'}';
}
}
2. 调用
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PersonTwo p = new PersonTwo("zhang");
Son son = new Son("实验二小");
p.setSon(son);
PersonTwo p1 = null;
p1 = (PersonTwo) p.clone();
p1.setName("yang");
son.setSchoolName("希望小学");
System.out.println("----" + p.toString());
System.out.println("----" + p1.toString());
}
3. 打印
System.out:----Person{name='zhang', mSon=Son{schoolName='希望小学'}}
System.out: ----Person{name='yang', mSon=Son{schoolName='实验二小'}}
4. 总结
深拷贝到达了我们想要的效果,拷贝的时候所有的引用都变了
原型模式示例
日常生活中,我们总接收或者发送邮件,如果让我们来写一个程序发送邮件怎么写。
1. 先来定义个发送邮件的类
/**
* 邮件模板Bean
*
* @author [email protected]
* @created 2018/7/13 下午3:02.
*/
public class Mail {
public String receiver;// 接收者
public String tail;// 结尾备注
private String context; // 内容
private String sub; // 标题
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getTail() {
return tail;
}
public void setTail(String tail) {
this.tail = tail;
}
public String getContext() {
return context;
}
public void setContext(String context) {
this.context = context;
}
public String getSub() {
return sub;
}
public void setSub(String sub) {
this.sub = sub;
}
}
2. 发送邮件
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* 发送邮件 */
final Mail mail = new Mail();
mail.setTail("xxx银行的所有版权");
for (int i = 0; i < 100; i++) {
mail.setSub("i" + " 先生(女士) ");
mail.setReceiver("i0001122" + "@qq.com");
sendMail(mail);
}
}
public static void sendMail(Mail mail) {
System.out.println("标题: " + mail.getSub() + "\t收件人"
+ mail.getReceiver() + "\t....发送成功! ");
}
}
3. 存在问题
如果使用单线程发送,按照一封邮件发出去需要 0.03秒, 1000封邮件需要30秒,可以接受。100万封邮件呢?大概就是几小时,肯定行不通。那么我们换成多线程,将sendMail任务封装,但是问题有来了,因为用的是同一个对象,如果线程1还没发送完,线程2执行了,那么就造成了数据错乱,线程1发送的名字变成了线程2的。
4. 解决办法
使用mail.clone()将对象拷贝一份,产生一个新的对象,和原有对象一样,然后再修改细节的数据,如设置称谓,设置收件人地址等等。而这种不通过 new 关键字来产生一个对象,而是通过对象拷贝来实现的模式就叫做原型模式。
/**
* 邮件模板Bean
*
* @author [email protected]
* @created 2018/7/13 下午3:02.
*/
public class Mail implements Cloneable {
.......
.......
.......
// 进行浅拷贝
@Override
protected Mail clone() throws CloneNotSupportedException {
Mail mail = (Mail) super.clone();
return mail;
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0; i < 100; i++) {
Mail cloneMail;
try {
cloneMail = mail.clone();
cloneMail.setSub("i" + " 先生(女士) ");
cloneMail.setReceiver("i0001122" + "@qq.com");
sendMail(cloneMail);
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void sendMail(Mail mail) {
System.out.println("标题: " + mail.getSub() + "\t收件人"
+ mail.getReceiver() + "\t....发送成功! ");
}
}
总结
- 对象拷贝时,类的构造函数是不会被执行的。
一个实现了 Cloneable 并重写了 clone 方法的类 A,有一个构造函数B,通过 new 关键字产生了一个对象 S,再然后通过 S.clone()方式产生了一个新的对象 T,那么在对象拷贝时构造函数 B 是不会被执行的,Object 类的 clone 方法的原理是从内存中(具体的说就是堆内存)以二进制流的方式进行拷贝,重新分配一个内存块,那构造函数没有被执行也是非常正常的了。 - final 类型修饰的成员变量不能进行深拷贝
- 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone的方法创建一个对象,然后由工厂方法提供给调用者。原型模式先产生出一个包含大量共有信息的类,然后可以拷贝出副本,修正细节信息,建立了一个完整的个性对象。
参考博客
23中设计模式之_原型模式(深/浅拷贝)