导读

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

【设计模式】创建型模式-原型模式_深拷贝



原型模式(Prototype)


【设计模式】创建型模式-原型模式_原型模式_02



1、概念

将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。


问题:

如果你有一个对象,并希望生产与其完全相同的一个复制品,那么该怎么实现?

可能你会new一个新对象,然后遍历原始对象的所有成员变量,并将成员变量值复制到新对象中。

但是这样会有问题,因为如果这些对象中有私有成员变量,它们对外是不可见的,所以就无法复制。


解决方案:

所以就有了原型模式。原型模式就是将克隆过程委派给被克隆的实际对象。它为所有支持克隆的对象声明一个通用接口,该接口让你能够克隆对象,同时又无需将代码和对象所属类耦合。一般这个通用接口只包含一个克隆方法。


原型模式有两种拷贝对象的方式:浅拷贝与深拷贝。


浅拷贝与深拷贝:

  • 数据分为基本数据类型和引用数据类型。基本数据类型:数据直接存储在栈中;引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。
  • 浅拷贝:对于基本数据类型:直接复制数据值;对于引用数据类型:只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变
  • 深拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:开辟一个一模一样的新的内存空间,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象
  • 深拷贝相比于浅拷贝速度较慢并且花销较大。



2、优缺点

优点:

  • 性能提高(相比new一个对象)。当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
  • 在克隆对象时无需与原始对象所属的具体类相耦合。


缺点:

  • 必须实现Cloneable接口或者实现序列化Serializable接口。
  • 在实现深拷贝时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深拷贝,每一次对象的类对必须支持深拷贝,实现起来可能比较麻烦。



3、场景

适用场景:

  • 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
  • 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
  • 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。


已有场景:

  • Java里Object类的clone()方法。
  • 浅拷贝与深拷贝。
  • Spring框架中,创建bean的时候,有个标签,让你选择:Prototype和Singleton。Singleton就是单例模式,而Prototype就是原型模式。



4、代码实现

4.1、原型模式-浅拷贝

只需要原型类实现一个接口Cloneable,并重写接口中的clone()方法即可。

这个原型类一般就是我们定义的实体类:

@Data
@NoArgsConstructor
@AllArgsConstructor
/**
* 1、实现Cloneable接口
*/
public class PrototypeStudentVo implements Cloneable {
private String name;
private int age;
private List<Integer> scores;
/**
* 2、重写clone方法,直接调用父类(Object)的clone()方法
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}


测试:

public class MyTest {
public static void main(String[] args) throws CloneNotSupportedException {
List<Integer> scores = new ArrayList<>();
scores.add(80);
scores.add(90);
scores.add(86);

//先new一个对象(原始对象)
PrototypeStudentVo studentVo = new PrototypeStudentVo("张三", 25, scores);

//拷贝对象,调用clone()方法,生成一个克隆对象
PrototypeStudentVo cloneStudentVo = (PrototypeStudentVo) studentVo.clone();

//检查克隆对象与原始对象是否一致
System.out.println("原始对象为:" + studentVo);
System.out.println("克隆对象为:" + cloneStudentVo);
//结果显示:信息是一致的。

//然后比较这两个对象是否为同一个对象
System.out.println("两个对象是否为同一个对象:" + (cloneStudentVo == studentVo));
//结果显示:false。说明克隆对象与原始对象在堆中是两个不同的对象。

//然后修改一下克隆对象里的属性,看是否对原始对象产生影响。
//修改基本数据类型的属性age
cloneStudentVo.setAge(30);
//修改引用数据类型的属性list
List<Integer> scoreList = cloneStudentVo.getScores();
scoreList.add(98);
cloneStudentVo.setName("李四");
System.out.println("修改克隆对象属性后,原始对象为:"+studentVo);
System.out.println("修改克隆对象属性后,克隆对象为:"+cloneStudentVo);
//结果显示,原始对象的name,age属性不会受到影响,scores受到了影响。

//注意:String在这里也是不会受影响的,虽然String是引用类型,但是编译器对其做了特殊处理(使其和基本数据类型一样)


//然后比较这两个对象是否为同一个对象
System.out.println("两个对象的scores地址是否相同:" + (cloneStudentVo.getScores() == studentVo.getScores()));
//结果显示:true。
/**
* 结论1:当克隆完成时,实际就是在堆中创建一个新对象,即克隆对象与原始对象在堆中的地址不一样。
* 结论2:如果原始对象的内部有引用类型的变量,在克隆时只是复制对象的引用地址,所以更改其中一个值,另一个对象的值也会随之改变,这就是浅拷贝。
*/
}
}


注意:String在这里也是不会受影响的,虽然String是引用类型,但是编译器对其做了特殊处理(使其和基本数据类型一样)



4.2、原型模式-深拷贝

深拷贝就是在浅拷贝的基础上,把引用类型的成员变量也克隆即可。

深拷贝的两种方案:递归拷贝和对象序列化。


(1)递归拷贝

这个方式需要引用的对象要实现了Cloneable接口,并重写了clone方法,并且,如果引用对象中还有引用对象,就需要无限套娃,并且每个套娃都要实现Cloneable接口,并重写了clone方法。


首先,定义一个实现类TeacherVo:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeepCopyTeacherVo implements Cloneable{
private String name;
private Integer age;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}


然后定义一个实现类StudentVo,它也是实现Cloneable,它引用TeacherVo对象:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeepCopyStudentVo implements Cloneable{
private String name;
private int age;
public DeepCopyTeacherVo cloneTeacherVo;
@Override
protected Object clone() throws CloneNotSupportedException {
//学生中引用老师的对象,直接调用老师的克隆方法
DeepCopyStudentVo cloneStudentVo = (DeepCopyStudentVo) super.clone();
cloneStudentVo.cloneTeacherVo = (DeepCopyTeacherVo) cloneTeacherVo.clone();
return cloneStudentVo;
}
}


测试:

public class MyTest {
public static void main(String[] args) throws CloneNotSupportedException {
//先new一个对象(原始对象)
DeepCopyStudentVo studentVo = new DeepCopyStudentVo("张三同学", 25, new DeepCopyTeacherVo("黄老师",35));
//拷贝对象,调用clone()方法,生成一个克隆对象
DeepCopyStudentVo cloneStudentVo = (DeepCopyStudentVo) studentVo.clone();
System.out.println("原始对象为:"+studentVo);
System.out.println("克隆对象为:"+cloneStudentVo);
//结果显示:信息是一致的。
//然后比较这两个对象是否为同一个对象
System.out.println("两个对象是否为同一个对象:" + (cloneStudentVo == studentVo));
//结果显示:false。说明克隆对象与原始对象在堆中是两个不同的对象。
//然后修改一下克隆对象里的属性,看是否对原始对象产生影响。
//更改克隆对象Student的age属性(基本数据类型)
cloneStudentVo.setAge(18);
//重点:更改克隆对象Teacher的age属性(引用数据类型)
cloneStudentVo.cloneTeacherVo.setAge(45);
System.out.println("修饰数据后,原始对象为:"+studentVo);
System.out.println("修改数据后,克隆对象为:"+cloneStudentVo);
//结果显示:克隆对象的属性值都更改了,即进行了深拷贝。
}
}

测试结果:

【设计模式】创建型模式-原型模式_深拷贝_03




(2)对象序列化

序列化的思路就相当于请求操作系统来进行复制粘贴文件一样的,将对象序列化成对象流和数组流,然后再写回来,操作系统就会为我们自动全部复制一份,并且里面的引用对象也复制一份。

使用前,需要实现序列化接口Serializable。并且,不只是被复制的类需要实现这个接口,它所引用的对象也要实现这个接口。


首先,定义一个实现类,实现序列化接口Serializable,然后自定义一个克隆方法:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeepCloneStudentVo implements Serializable {
private String name;
private int age;
private List<Integer> scores;
/**
* 利用对象序列化自定义一个克隆方法
*
* @return
*/
public DeepCloneStudentVo deepCloneStudentVo() {
ByteArrayOutputStream baos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bais = null;
ObjectInputStream ois = null;
DeepCloneStudentVo studentVo = null;
try {
//输出(序列化)
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(this);
//输入(反序列化)
bais = new ByteArrayInputStream(baos.toByteArray());
ois = new ObjectInputStream(bais);
studentVo = (DeepCloneStudentVo) ois.readObject();
return studentVo;
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
baos.close();
bais.close();
oos.close();
ois.close();
} catch (Exception e1) {
System.out.println(e1.getMessage());
}
}
return studentVo;
}
}


测试:

public class MyTest {
public static void main(String[] args) {
List<Integer> scores = new ArrayList<>();
scores.add(80);
scores.add(90);
scores.add(86);
//先new一个对象(原始对象)
DeepCloneStudentVo studentVo = new DeepCloneStudentVo("张三", 25, scores);
//调用自定义的克隆方法。
DeepCloneStudentVo cloneStudentVo = studentVo.deepCloneStudentVo();
System.out.println("原始对象为:" + studentVo);
System.out.println("克隆对象为:" +cloneStudentVo);
//结果显示:信息是一致的。
//然后修改一下克隆对象里的属性,看是否对原始对象产生影响。
//修改基本数据类型的属性age
cloneStudentVo.setAge(30);
//重点:修改引用数据类型的属性list
List<Integer> scoreList = cloneStudentVo.getScores();
scoreList.add(98);
System.out.println("修改克隆对象属性后,原始对象为:"+studentVo);
System.out.println("修改克隆对象属性后,克隆对象为:"+cloneStudentVo);
//结果显示:结果显示:克隆对象的属性值都更改了,即进行了深拷贝。
}
}


测试结果:

【设计模式】创建型模式-原型模式_浅拷贝_04




5、总结

  • 原型模式就是将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。


  • 浅拷贝:对于基本数据类型:直接复制数据值;对于引用数据类型:只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变


  • 深拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:开辟一个一模一样的新的内存空间,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象


  • 对于浅拷贝,只需要原型类实现一个Cloneable接口,并重写接口中的clone()方法即可。


  • 对于深拷贝的递归拷贝方式,需要引用的对象要实现了Cloneable接口,并重写了clone方法,并且,如果引用对象中还有引用对象,每个引用对象都要实现Cloneable接口,并重写了clone方法。


  • 对于深拷贝的对象序列化方式,需要实现序列化接口Serializable。并且,不只是被复制的类需要实现这个接口,它所引用的对象也要实现这个接口。