zoukankan      html  css  js  c++  java
  • 设计模式:原型模式介绍 && 原型模式的深拷贝问题


    0、背景


    克隆羊问题:有一个羊,是一个类,有对应的属性,要求创建完全一样的10只羊出来。

    那么实现起来很简单,我们先写出羊的类:

    public class Sheep {
        private String name;
        private int age;
        private String color;
        //下面写上对应的get和set方法,以及对应的构造器
    }
    

    然后,创建10只一样的羊,就在客户端写一个代码创建:

     //原始羊
     Sheep sheep = new Sheep("tom",1,"白色"); 
     //克隆羊 
     Sheep sheep1 = new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor());
    

    sheep1 是克隆的第一只羊,接着就可以复制十遍这个代码,然后命名不同的羊,以原始sheep为模板进行克隆。

    这种方法的弊端:

    1. 创建新对象,总是需要重新获取原始对象的属性值,效率低;
    2. 总是需要重新初始化对象,而不是动态获取对象运行时的状态,不灵活。(什么意思呢,比如原始的 Sheep 有一项要修改,那么剩下的以它为范本的,必然要重新初始化)

    一、原型模式


    1. 原型模式指的是,用原型实例指定创建对象的种类,并通过拷贝这些原型,创建新的对象;
    2. 原型模式是一种创建型设计模式,允许一个对象再创建另一个可以定制的对象,无需知道如何创建的细节;
    3. 工作原理是:发动创建的这个对象,请求原型对象,让原型对象来自己实施创建,就是原型对象.clone()

    如下类图所示:

    其中,Prototype 是一个原型接口,在这里面把克隆自己的方法声明出来;
    ConcreteProtype 可以是一系列的原型类,实现具体操作。

    java 的 Object 类是所有类的根类,Object提供了一个 clone() 方法,该方法可以将一个对象复制一份,但是想要实现 clone 的 java 类必须要实现 Cloneable 接口,实现了之后这个类就具有复制的能力。

    对于克隆羊问题,我们来利用原型设计模式进行改进:

    让Sheep类,实现 Cloneable 接口:

    public class Sheep implements Cloneable{
        private String name;
        private int age;
        private String color;
    
        //getters&&setters&&constructors
        
        @Override
        protected Object clone() {
            Sheep sheep = null;
            try {
                sheep = (Sheep)super.clone();//使用默认Object的clone方法来完成
            } catch (CloneNotSupportedException e) {
                System.out.println(e.getMessage());
            }
            return sheep;
        }
    }
    

    现在的 Sheep 类就是一个具体的原型实现类了,我们想要克隆的时候,客户端调用可以这样:

    Sheep sheep1 = (Sheep) sheep.clone();
    Sheep sheep2 = (Sheep) sheep.clone();
    //。。。。。类似
    

    这种做法就是原型设计模式。

    (spring框架里,通过bean标签配置类的scope为prototype,就是用的原型模式)


    二、原型模式的浅拷贝、深拷贝问题


    使用上面所说的原型模式,按理说是复制出了一模一样的对象。

    但我们做一个尝试,如果 sheep 类里的成员变量有一个是对象,而不是基础类型呢

    private Sheep friend;
    

    然后我们创建、再克隆:

    Sheep sheep = new Sheep("tom",1,"白色");//原始羊
    sheep.setFriend(new Sheep("jack",2,"黑色"));
    Sheep sheep1 = (Sheep) sheep.clone();
    Sheep sheep2 = (Sheep) sheep.clone();
    Sheep sheep3 = (Sheep) sheep.clone();
    

    重写一下 Sheep 类的 toString 方法,输出信息和对应的属性的 hashcode 后会发现:

    Sheep{name='tom', age=1, color='白色', friend=488970385}
    Sheep{name='tom', age=1, color='白色', friend=488970385}
    Sheep{name='tom', age=1, color='白色', friend=488970385}
    

    friend 的 hashCode 值都一样,也就是克隆的类的 friend 属性其实没有被复制,而是指向了同一个对象。

    这就叫浅拷贝(shallow copy):

    1. 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是复制一份给新对象;
    2. 对于数据类型是引用数据类型的成员变量,浅拷贝会进行引用传递,也就是只是将地址指针复制一份给新对象,实际上复制前和复制后的内容都指向同一个实例。这种情况,显然在一个对象里修改成员变量,会影响到另一个对象的成员变量值(因为修改的都是同一个)
    3. 默认的 clone() 方法就是浅拷贝。

    在源码里也说明了,这个方法是shallow copy 而不是 deep copy

    在实际开发中,往往是希望克隆的过程中,如果类的成员是引用类型,也能完全克隆一份,也就是所谓的深拷贝

    深拷贝(Deep Copy):

    1. 复制对象的所有基本数据类型成员变量值;
    2. 为所有 引用数据类型 的成员变量申请存储空间,并且也复制每个 引用数据类型的成员变量 引用的 所有对象,一直到该对象可达的所有对象;

    深拷贝的实现方式,需要通过重写 clone 方法,或者通过对象的序列化。

    下面来实现一下。


    2.1 通过重写 clone 方法深拷贝

    /*
        被拷贝的类引用的类,此类的clone用默认的clone即可
    */
    public class CloneTarget implements Cloneable {
        private static final long serialVersionUID = 1L;
        private String cloneName;
        private String cloneClass;
    
        public CloneTarget(String cloneName, String cloneClass) {
            this.cloneName = cloneName;
            this.cloneClass = cloneClass;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    
    /*
        原型类,其中有成员是引用类型,因此clone方法要重写达到深拷贝
    */
    public class Prototype implements Cloneable {
        public String name;
        public CloneTarget cloneTarget;
        public Prototype() {
            super();
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            Object o = null;
            //用了浅拷贝,基本数据克隆完成,但是cloneTarget指向的还是原来的对象
            o = super.clone();
            //单独处理引用类型
            Prototype target = (Prototype) o;
            target.cloneTarget = (CloneTarget)cloneTarget.clone();
            return target;
        }
    }
    

    这样的话,新建一个原型Prototype的对象后,对他进行克隆,得到的里面的 CloneTarget 成员也是深拷贝的两个不一样的对象了。

    但是这种方法本质上是相当于 套娃 ,因为都要单独处理重写 clone 方法,所以有些麻烦。


    2.2 通过对象的序列化

    在 Prototype 里直接 使用序列化+反序列化,达到对这个对象整体的一个复制。

    另外注意,序列化和反序列化,必须实现 Serializable 接口,所以 implements 后面不止要有 Cloneable,还有Serializable。

    //利用序列化实现深拷贝
    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);
            Prototype copy = (Prototype) ois.readObject();
            return copy;
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            try {
                bos.close();
                oos.close();
                bis.close();
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    

    然后我们想要克隆的时候,直接调用这个 deepClone 方法就可以达到目的。

    忽视掉里面的 try - catch 之类的代码,其实核心部分就是用到序列化和反序列化的总共 4 个对象。这种方法是推荐的,因为实现起来更加容易。

    序列化反序列化达到深拷贝目的的原理:

    • ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream,但是只能将支持 java.io.Serializable 接口的对象写入流中。

    在这里,我们采用的OutputStream是ByteArrayOutputStream——字节数组输出流,通过创建的ObjectOutputStream的writeObject方法,把对象写进了这个字节数组输出流。

    • 相对应的,ObjectInputStream反序列化原始数据,恢复以前序列化的那些对象。

    在这里,把字节数组重新构造成一个ByteArrayInputStream——字节数组输入流,通过ObjectInputStream的readObject方法,把输入流重新构造成一个对象。

    结合上面的代码再看看:

    bos = new ByteArrayOutputStream();
    oos = new ObjectOutputStream(bos);//写入指定的OutputStream
    oos.writeObject(this);//把对象写入到输出流中,整个对象,this
    
    bis = new ByteArrayInputStream(bos.toByteArray());
    ois = new ObjectInputStream(bis);//读取指定的InputStream
    Prototype copy = (Prototype) ois.readObject();//从输入流中读取一个对象
    
    return copy;
    

    三、总结


    原型模式:

    1. 当需要创建一个新的对象的内容比较复杂的时候,可以利用原型模式来简化创建的过程,同时能够提高效率。
    2. 因为这样不用重新初始化对象,而是动态地获得对象运行时的状态,如果原始的对象内部发生变化,其他克隆对象也会发生相应变化,无需一 一修改。
    3. 实现深拷贝的方法要注意。

    缺点:

    每一个类都需要一个克隆方法,对于全新的类来说不是问题,但是如果是用已有的类进行改造,那么可能会因为要修改源代码而违背 OCP 原则。

  • 相关阅读:
    I Hate It
    满减优惠[Offer收割]编程练习赛4
    积水的城市 hiho[Offer收割]编程练习赛4
    Subsequence 尺取法
    526. 优美的排列
    401. 二进制手表
    306. 累加数
    216. 组合总和 III
    131. 分割回文串
    ubuntu deepin-软件 分辨率的问题
  • 原文地址:https://www.cnblogs.com/lifegoeson/p/13491004.html
Copyright © 2011-2022 走看看