寂然解读设计模式 - 原型模式(下)


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, 下一节我们进入下一个模式 - 建造者模式的学习,希望大家在学习的过程中,能够感觉到设计模式的有趣之处,高效而愉快的学习,那我们下期见~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容