I walk very slowly, but I never walk backwards
设计模式 - 原型模式(下)
寂然
大家好~,我是寂然,本节课呢,我们对原型模式进行深入一点的讨论,我们来聊聊深拷贝和浅拷贝
前情提要
上节课,我们聊了聊小李求职的事情,写了一个关于原型模式的案例,大家有没有发现,我们使用原型模式创建类 Resume 的对象时,类 Resume 的属性,都是 String 类型,换而言之,都是简单类型,那假设,现在增加了一个属性,简历上需要有一个证明人,是 Witness 类型的对象,那大家一起来思考这样一个问题,当再次进行原型拷贝的时候,属性 witness 会怎么去处理呢?是复制一份,还是让一个引用去指向已有的 witness?觉知此事要躬行,下面,我们通过案例代码来进行测试
案例演示
//简历类
public class Resume implements Cloneable{
private String name;
private String position;
private String salary;
public Witness witness; //证明人
//构造器/get/set/toString...省略
}
//证明人类
public class Witness {
private String name;
private String job; //职务
//构造器/get/set/toString...省略
}
//使用原型模式测试
public class Client {
public static void main(String[] args) {
Resume resume = new Resume("小李", "海淀区", "面议");
resume.setWitness(new Witness("三叔","养猪场CTO"));
Resume clone = (Resume)resume.clone(); //使用原型模式克隆两份
Resume clone1 = (Resume)resume.clone();
Resume clone2 = (Resume)resume.clone();
System.out.println(clone.getWitness().hashCode()); //测试
System.out.println(clone1.getWitness().hashCode());
System.out.println(clone2.getWitness().hashCode());
}
}
可以看到,我们使用原型模式克隆了三份,发现三个成员变量 witness 的哈希值是相同的,也就是说,默认并没有对属性 witness 进行拷贝,而是通过一个引用去指向已有的 witness,这种情况就称为浅拷贝
浅拷贝
下面,我们来一起看一下浅拷贝的介绍
对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,就是将该属性值复制一份给新的对象
对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象,实际上两个对象的该成员变量都指向同一个实例,这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
所以,我们前面讲的小李求职的案例就是浅拷贝,浅拷贝就是使用默认的 clone() 方法实现的
那如果说,实际情况中,我们希望对于数据类型是引用数据类型的成员变量,也复制一份呢?那对比着再来聊一下深拷贝,以及深拷贝的实现方式
深拷贝
深拷贝的基本介绍如下
复制对象的所有基本数据类型的成员变量值
为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象,也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝
深拷贝在实际开发中有两种实现方式,一种是通过重写 clone () 方法实现,一种是通过对象序列化实现
实现方式一:重写 clone() 方法
示例代码如下图所示
public class DeepCloneableTarget implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
//因为该类的属性都是String,所以我们使用默认的clone方法完成即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class DeepProtoType implements Serializable, Cloneable {
private String name; //String类型
private DeepCloneableTarget deepCloneableTarget; //引用类型
public DeepProtoType() {
super();
}
//深拷贝 - 方式一:重写clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object deep = null;
//这里完成的是对基本数据类型以及String类型的拷贝
deep = super.clone();
//对引用类型的属性进行单独处理
DeepProtoType deepProtoType = (DeepProtoType) deep;
deepProtoType.deepCloneableTarget= (DeepCloneableTarget) deepCloneableTarget.clone();
return deepProtoType;
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
DeepProtoType deepProtoType = new DeepProtoType();
deepProtoType.name = "小李";
deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆类");
//方式一完成深拷贝
DeepProtoType clone = (DeepProtoType) deepProtoType.clone();
DeepProtoType clone1 = (DeepProtoType) deepProtoType.clone();
System.out.println(clone.deepCloneableTarget.hashCode());
System.out.println(clone1.deepCloneableTarget.hashCode()); //哈希值不一样
}
}
可以看到,经测试,哈希值不一样,说明确实整个对象(包括对象的引用类型)都进行了拷贝 ,实现了深拷贝
重写 clone() 方法完成深拷贝的思路就是,分步进行,在 clone() 方法里,首先调用 super.clone() 完成对基本类型和 String 类型的拷贝,因为对于基本数据类型的成员变量,深拷贝和浅拷贝的处理方式一致,都是将该属性值复制一份给新的对象,接着我们对引用类型的属性进行单独处理,同样使用 clone() 方法完成属性值的拷贝
那这样有的小伙伴要问了,如果 DeepProtoType 有很多个引用类型的成员属性,都要这样挨个使用 clone() 方法完成属性值的拷贝嘛?是的,这种情况下,每个引用类型需要单独处理,那还有的小伙伴问了,如果引用类型是个类,里面仍然有引用类型的成员属性呢?
级联处理演示
如果引用类型是个class,类型,里面仍然有引用类型的成员属性同样,核心思想是分布单独处理,假设类 DeepProtoType 里有成员属性 deepCloneableTarget ,而DeepCloneableTarget 类中有成员属性 witness ,同样使用方式一,重写 clone() 方法完成深拷贝,示例代码如下图所示
//方式一升级:测试级联克隆
public class DeepProtoType implements Serializable, Cloneable {
public String name; //String类型
public DeepCloneableTarget deepCloneableTarget; //引用类型
public DeepProtoType() {
super();
}
//深拷贝 - 方式一:重写clone方法 级联
@Override
protected Object clone() throws CloneNotSupportedException {
Object deep = null;
//这里完成的是对基本数据类型以及String类型的拷贝
deep = super.clone();
//对引用类型的属性进行单独处理
DeepProtoType deepProtoType = (DeepProtoType) deep;
deepProtoType.deepCloneableTarget= (DeepCloneableTarget) deepCloneableTarget.clone();
deepProtoType.deepCloneableTarget.witness = (Witness) deepProtoType.deepCloneableTarget.witness.clone();
return deepProtoType;
}
@Override
public String toString() {
return "DeepProtoType{" +
"name='" + name + '\'' +
", deepCloneableTarget=" + deepCloneableTarget +
'}';
}
}
public class DeepCloneableTarget implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public Witness witness; //增加引用类型
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
//因为该类的属性都是String,所以我们使用默认的clone方法完成即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "DeepCloneableTarget{" +
"cloneName='" + cloneName + '\'' +
", cloneClass='" + cloneClass + '\'' +
", witness=" + witness +
'}';
}
}
//证明人类
public class Witness implements Serializable,Cloneable {
private String name;
private String job; //职务
public Witness(String name, String job) {
this.name = name;
this.job = job;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
@Override
public String toString() {
return "Witness{" +
"name='" + name + '\'' +
", job='" + job + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//方式一升级:测试级联克隆
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
DeepProtoType deepProtoType = new DeepProtoType();
deepProtoType.name = "小李";
deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆类");
deepProtoType.deepCloneableTarget.witness = new Witness("证明人", "职务");
DeepProtoType clone = (DeepProtoType) deepProtoType.clone();
DeepProtoType clone1 = (DeepProtoType) deepProtoType.clone();
System.out.println(clone);
System.out.println(clone1);
System.out.println(clone.deepCloneableTarget.witness.hashCode());
System.out.println(clone1.deepCloneableTarget.witness.hashCode());
//测试哈希值不一样 实现了深拷贝
}
}
实现方式二:通过对象序列化
上面我们演示了方式一,实现了深拷贝,但同时也带来了问题,如果有很多个引用类型的成员属性,或者说(引用类型是个类,里面仍然有引用类型的成员属性)这种级联的情况,需要分步处理,如果要拷贝的对象结构复杂,实现起来非常繁琐,那有没有可以一步到位的方法呢?下面我们来演示第二种,使用对象序列化的形式,示例代码如
下图所示
public class DeepProtoType implements Serializable, Cloneable {
public String name; //String类型
public DeepCloneableTarget deepCloneableTarget; //引用类型
public DeepProtoType() {
super();
}
//深拷贝 - 方式二:通过对象序列化(推荐的)
public Object deepClone(){
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);//将当前对象,以对象流的方式输出(即序列化)
//输出的时候会把包括引用类型一起输出
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());//把输出去的对象再读进来
ois = new ObjectInputStream(bis);
DeepProtoType clone = (DeepProtoType)ois.readObject();
//充分利用序列化和反序列化的特点,把当前对象this以对象流的方式输出去,然后以对象的方式再读回来,关联的引用类型自然也读回来了
return clone;
} catch (Exception e) {
e.getMessage();
} finally {
//关闭流操作
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (IOException e) {
e.getMessage();
}
}
return null;
}
@Override
public String toString() {
return "DeepProtoType{" +
"name='" + name + '\'' +
", deepCloneableTarget=" + deepCloneableTarget +
'}';
}
}
public class DeepCloneableTarget implements Serializable, Cloneable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
//因为该类的属性都是String,所以我们使用默认的clone方法完成即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "DeepCloneableTarget{" +
"cloneName='" + cloneName + '\'' +
", cloneClass='" + cloneClass + '\'' +
'}';
}
}
// 测试 方式二 通过对象序列化
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
DeepProtoType deepProtoType = new DeepProtoType();
deepProtoType.name = "小李";
deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆类");
DeepProtoType clone = (DeepProtoType)deepProtoType.deepClone();
DeepProtoType clone1 = (DeepProtoType)deepProtoType.deepClone();
System.out.println(clone.toString());
System.out.println(clone1.toString());
System.out.println(clone.deepCloneableTarget.hashCode());
System.out.println(clone1.deepCloneableTarget.hashCode()); //测试哈希值不一致
}
}
可以看到,经测试,哈希值不一样,说明确实整个对象(包括对象的引用类型)都进行了拷贝 ,我们通过对象序列化的方式也实现了深拷贝,而且利用序列化和反序列化的特点,把当前对象this以对象流的方式输出去,然后以对象的方式再读回来,关联的引用类型的属性值自然也读回来了,这种方式是推荐大家使用的,因为是直接把当前整个对象进行序列化和反序列化操作,那不管类的结构如何复杂,都可以通过对象序列化的方式整体处理
注意事项
原型模式到这里就告一段落了,下面我们一起来总结下原型模式的注意事项,分析下优劣势以及应用场景
优势
创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也可以提高效率
不用重新初始化对象,而是动态地获得对象运行时的状态
如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码
劣势
需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,会违背 ocp 原则,这点需要和小伙伴们提一下
下节预告
OK, 下一节我们进入下一个模式 - 建造者模式的学习,希望大家在学习的过程中,能够感觉到设计模式的有趣之处,高效而愉快的学习,那我们下期见~