原型模式 深拷贝/浅拷贝
原型模式 :即通过clone模式将原有对象复制成一个新对象,来代替使用new的方式创建对象
好处就是,当创建一个大的对象时,使用new的方式内存开销很大,因此可以采用clone方式直接复制一个对象
下面通过代码来测试一下:
代码属于伪代码,缺少get,set即toString方法
//Object类实际上是有clone方法的,但是它是被声明成被保护的,用protected修饰的,因此需要实现Cloneable接口,
//实现Cloneable,此接口没有实现方法,只起到标记的作用,然后在类中重写Object中的clone方法,调用父类的clone方法
public class Phone implements Cloneable{
private String type;
private double screen;
private Photo photo;
public Phone clonePhone(){
Phone clonePhone=null;
try {
clonePhone= (Phone) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clonePhone;
}
public Phone(String type,double screen,Photo photo){
this.type=type;
this.screen =screen;
this.photo=photo;
try {
//此处模拟生成对象消耗的时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Photo类,此时没有用可以暂时不去关注,后续会用来区分深拷贝和浅拷贝
public class Photo {
private long pixel;
private double size;
public Photo(long pixel, double size) {
this.pixel = pixel;
this.size = size;
}
}
然后编写测试方法,查看其运行效率
@org.junit.Test
public void test1(){
int maxCount=10;
int i=0;
//使用new来创建十个Phone对象耗时
long startTime=System.currentTimeMillis();
while(i<maxCount){
Phone phone = new Phone("Aphone",5.5,new Photo(1000,10.5));
phone.sendSms("发送短信",String.valueOf(i));
i++;
}
long endTime=System.currentTimeMillis();
System.out.println("new方式创建对象耗时:"+(endTime - startTime)+"ms");
//使用clone模式来创建十个Phone对象耗时
long startCloneTime=System.currentTimeMillis();
Phone phone = new Phone("Aphone",5.5,new Photo(1000,10.5));
while(i<maxCount){
Phone clonePhone = phone.clone();
clonePhone.setScreen(0);
clonePhone.getPhoto().setSize(0);
clonePhone.sendSms("发送短信",String.valueOf(i));
i++;
}
long endCloneTime=System.currentTimeMillis();
System.out.println("clone方式创建对象耗时:"+(endCloneTime - startCloneTime)+"ms");
}
显然new对象要比clone方式消耗时间长很多,因此在需要创建多个同一的大对象时,尽量使用clone方式生成
上述方式只是一个浅拷贝的方式,它会将所有的属性拷贝一份放入新对象中。如果是基本数据类型,新对象属性的变化不会影响到原对象属性值;但是当它的属性时引用类型时,浅拷贝会将引用的实际内存地址,即对象在堆上分配的内存地址,复制一份给新对象的实例变量,并没有在堆上重新开辟一块区域。这时,实际上新对象和原对象其实指向的是同一个内存地址,其中一个改变对象的属性,就会影响到另一个对象。下面可以验证一下
首先,Phone类和Photo类不变,创建一个测试方法来观察他们的属性变化
@org.junit.Test
public void test2() {
int i = 0;
Phone phone = new Phone("Aphone", 5.5, new Photo(1000, 10.5));
System.out.println("原手机对象:" + phone);
Phone clonePhone = phone.clone();
//给克隆对象基本类型设置参数
clonePhone.setScreen(0);
//获取克隆对象的成员属性Photo对象,并重新给其赋值
clonePhone.getPhoto().setSize(0);
System.out.println("克隆对象:" +clonePhone);
clonePhone.sendSms("发送短信", String.valueOf(i));
System.out.println("克隆对象重新对变量进行赋值后,原手机对象:"+phone);
}
运行结果
运行结果中,克隆对象的基本数据类型screen修改为0.0,并没有影响原对象该属性。而对其引用变量的属性重新赋值时导致,原对象此引用变量的属性值发生了更改。因此浅拷贝会破坏原有对象的引用变量,会造成一定的混乱,因此一般都会采用深拷贝的方式赋值对象,比如在mybatis框架源码中,在将标签解析并放入MappedStatement对象时,在分析sql过程中,如果发现sql中有标签时,会从上一步生成的此标签节点对象深拷贝一份,用来做后续处理,避免污染原有对象。如下图所示,
再贴一张上面描述的和标签,以免过于抽象
深拷贝: 简单地说,深拷贝就是对引用数据类型的成员变量的对象开辟了新的内存空间,当引用的对象里还有引用时也会继续向下递归开辟新的内存空间。
实现方式:
第一种,既然浅拷贝可以正常复制基本数据类型,因此可以用递归的方式,对成员变量是引用的对象再实现CloneAble接口,对其属性也进行克隆;上代码:
首先对Photo类进行改造,让其实现Cloneable接口,并重写clone方法
public class Photo implements Cloneable{
private long pixel;
private double size;
public Photo(long pixel, double size) {
this.pixel = pixel;
this.size = size;
}
public Photo clone(){
Photo clonePhoto=null;
try {
clonePhoto= (Photo) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clonePhoto;
}
}
然后,修改Phone的clone方法,通过调用photo对象的clone方法,重新为photo属性赋值
public class Phone implements Cloneable{
private String type;
private double screen;
private Photo photo;
public Phone clone(){
Phone clonePhone=null;
try {
clonePhone= (Phone) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
clonePhone.setPhoto(this.photo.clone());
return clonePhone;
}
}
最后,测试一下
@org.junit.Test
public void test3() {
Phone phone = new Phone("Aphone", 5.5, new Photo(1000, 10.5));
System.out.println("原手机对象:" + phone);
Phone clonePhone = phone.clone();
//给克隆对象基本类型设置参数
clonePhone.setScreen(0);
//获取克隆对象的成员属性Photo对象,并重新给其赋值
clonePhone.getPhoto().setSize(0);
System.out.println("克隆对象:" +clonePhone);
System.out.println("克隆对象重新对变量进行赋值后,原手机对象:"+phone);
}
运行结果:
结果显示,克隆对象中引用变量的变化并未影响到原对象的值。
但是,这种方式有一种弊端就是,我所有的对象都要去实现Cloneable并重写Object的clone()方法,这样就回导致代码复杂度提高,当对象过多时或这引用太深时,代码会越来越混乱,不易维护。但是此方法比较容易实现对象的clone
第二种方式:
使用流来对对象进行复制,这也是用的比较多的clone方式。
@org.junit.Test
public void test4(){
Phone phone = new Phone("Aphone", 5.5, new Photo(1000, 10.5));
System.out.println("原对象:"+phone);
ByteOutputStream bos= new ByteOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(phone);
oos.flush();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Phone clonePhone= (Phone) ois.readObject();
ois.close();
clonePhone.setScreen(0);
clonePhone.getPhoto().setSize(0);
System.out.println("克隆对象属性变化后,原对象:"+phone);
System.out.println("克隆对象:"+clonePhone);
} catch (Exception e) {
e.printStackTrace();
}
}
这种方式下,需要被克隆的类无需实现Cloneable接口以及重写clone方法,大大减少的了重复代码,不需要对每个类都进行特殊处理,只需在需要克隆对象的地方自行序列化/反序列化克隆即可。
//伪代码,要想使用流处理对对象进行序列化和反序列化,必须要实现Serializable 标志此类可以被序列化,如果没有实现此接口,将会抛出异常
public class Phone implements Serializable {
private static final long serialVersionUID = 8030191510212419448L;
private String type;
private double screen;
private Photo photo;
}
public class Photo implements Serializable {
private static final long serialVersionUID = -5894538960278714681L;
private long pixel;
private double size;
}
运行结果正常,与第一种克隆结果一致
关于基本数据类型为什么可以直接复制,并重新赋值,网上说的是因为基本数据类型采用的是值传递的形式,但是我觉得应该跟基本数据类型在jvm中存储区域有关,目前还在学习中,请多多指教!