设计模式:原型模式介绍 && 原型模式的深拷贝问题

2020-08-12

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