当我们有时在系统中需要重复多次创建对象,且对象属于同一类型,对象的构造(初始化)比较复杂耗时,这时候就可以使用原型模式创建(ps:单例、工厂和原型有一个共同点就是创建对象的初始化过程都很复杂的时候,不同的是单例只创建一个,而原型和工厂针对的是创建多个,工厂一般用于初始化一个对象所需要的信息不确定的情况下,而原型模式则是克隆一个已经初始化好的对象)。
解决方案:通过拷贝某个目标对象,创建该类型的新的对象。
我相信很多人在java基础中都学习过对象的深复制和浅复制的区别,其实原型模式就是深复制和浅复制的利用,因为原型模式最后的目的也是为了创建对象。
我们知道在Java中存在接口Cloneable,实现该接口的类都会具备被拷贝的能力,拷贝是在内存中进行,所以性能方面比我们直接new生成快。拷贝分为深拷贝和浅拷贝。
首先介绍的是浅拷贝:
public class Person implements Cloneable{
private String name;
private Email email;
public Person(String name,Email email){
this.name = name;
this.email = email;
}
//get、set略…
//实现克隆方法
protected Person clone() {
Person person = null;
try {
person = (Person) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return person;
}
}
public class Test {
public static void main(String[] args) {
//Email略
Email email = new Email("下午四点开会");
Person zhangsan = new Person("张三",email);
Person lisi = zhangsan.clone();
lisi.setName("李四");
Person wanwu = zhangsan.clone();
wanwu.setName("王五");
System.out.println(zhangsan.getName() + zhangsan.getEmail().getInfo());
System.out.println(lisi.getName() + lisi.getEmail().getInfo());
System.out.println(wanwu.getName() + wanwu.getEmail().getInfo());
}
}
控制台输出:
张三的邮件内容是:张三下午四点开会
李四的邮件内容是:李四下午四点开会
王五的邮件内容是:王五下午四点开会
创建张三对象之后,李四和王五只需要复制张三的对象并修改名称属性即可、假设现在需要张三提前10分钟到,内容修改下:
public class Client {
public static void main(String[] args) {
Email email = new Email("下午四点开会");
Person zhangsan = new Person("张三",email);
Person lisi = zhangsan.clone();
lisi.setName("李四");
Person wanwu = zhangsan.clone();
wanwu.setName("王五");
//复制完李四和王五的对象之后,再改变张三的对象内容
zhangsan.getEmail().setInfo("下午四点开会,提前十分钟入场")
//输出:
System.out.println(zhangsan.getName() + zhangsan.getEmail().getInfo());
System.out.println(lisi.getName() + lisi.getEmail().getInfo());
System.out.println(wanwu.getName() + wanwu.getEmail().getInfo());
}
}
控制台输出:
张三的邮件内容是:张三下午四点开会,提前十分钟入场
李四的邮件内容是:李四下午四点开会,提前十分钟入场
王五的邮件内容是:王五下午四点开会,提前十分钟入场
为什么李四和王五的邮件内容也发送了改变?问题的关键就在于clone()方法上,因为clone()方法是使用Object类的clone()方法,它并不会将对象的所有属性全部拷贝过来,而是有选择性的拷贝,基本规则如下:
1、 基本类型:如果变量是基本类型,则拷贝其值,比如int、float等。但是若变量为String字符串,则拷贝其地址引用。但是在修改时,它会从字符串池中重新生成一个新的字符串,原有字符串对象保持不变。
2、 对象:如果变量是一个实例对象,则拷贝其地址引用,也就是说此时新对象与原来对象是公用该实例变量。
知道原因周很容易发现问题的所在,李四和王五,是从张三拷贝过来的,张三类里的Email类不是基本类型,所以拷贝其地址引用,导致三者共用(共同指向)一个Email对象实例,张三修改了邮件内容,李四、王五也会变化,所以才会出现上面的情况。那如何解决呢,只需要在复制对象的时候,让新对象直接引用新的Email对象即可
protected Person clone() {
Person person = null;
try {
person = (Person) super.clone();
person.setEmail(new Email(person.getEmail().getInfo()));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return person;
}
以上拷贝称之为浅拷贝,不便于直接使用。因为如果我们每一个类都写一个独立的clone()方法,大量的对象时,工作量也会增加,所有使用深拷贝(对象序列化)。
二、利用序列化实现对象的深拷贝
如何利用序列化来完成对象的深拷贝呢?在内存中通过字节流的拷贝是比较容易实现的。把母对象写入到一个字节流中,再从字节流中将其读出来,这样就可以创建一个新的对象了,并且该新对象与目标对象之间并不存在引用共享的问题。
public class CloneUtils {
public static <T> T clone(T obj){
T cloneObj = null;
try {
//将obj对象写出到out字节流之中
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
//分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
注意:
使用该工具类的对象必须要实现Serializable接口,无须继承Cloneable。
public class Person implements Serializable{
//去除clone()方法
}
public class Email implements Serializable{
....................
}
public class Client {
public static void main(String[] args) {
Email email = new Email("下午四点开会");
Person zhangsan = new Person("张三",email);
Person lisi = CloneUtils.clone(zhangsan );
lisi.setName("李四");
Person wangwu = CloneUtils.clone(zhangsan);
wangwu.setName("王五");
zhangsan.getEmail().setInfo("下午四点开会,提前十分钟到场");
System.out.println(person1.getName() + "的邮件内容是:" + person1.getEmail().getContent());
System.out.println(person2.getName() + "的邮件内容是:" + person2.getEmail().getContent());
System.out.println(person3.getName() + "的邮件内容是:" + person3.getEmail().getContent());
}
}
控制台输出:
张三的邮件内容是:张三下午四点开会,提前十分钟到场
李四的邮件内容是:李四下午四点开会
王五的邮件内容是:王五下午四点开会