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 原则。

  • 相关阅读:
    iaas,paas,saas理解
    July 06th. 2018, Week 27th. Friday
    July 05th. 2018, Week 27th. Thursday
    July 04th. 2018, Week 27th. Wednesday
    July 03rd. 2018, Week 27th. Tuesday
    July 02nd. 2018, Week 27th. Monday
    July 01st. 2018, Week 27th. Sunday
    June 30th. 2018, Week 26th. Saturday
    June 29th. 2018, Week 26th. Friday
    June 28th. 2018, Week 26th. Thursday
  • 原文地址:https://www.cnblogs.com/lifegoeson/p/13491004.html
Copyright © 2011-2022 走看看