创建型设计模式(4):原型模式

和自甴很熟 提交于 2020-01-14 23:45:45


类和对象的关系好比模具和构件的关系,对象总是通过构造方法从类中创建的。

但是某些场景下是不允许类的调用者直接调用构造函数,也就说对象未必需要从类中衍生出来,现实生活中存在太多案例是通过直接 “克隆” 来产生新的对象,而且克隆出来的本体和克隆体看不出任何区别。

原型模式不单是一种设计模式,也是一种编程范型。简单理解原型模式 Prototype:不根据类来生成实例,而是根据已有的实例对象生成新的实例对象

1.复制引用、复制对象、浅拷贝和深拷贝

首先定义一个类,

import java.util.Date;

public class Person implements Cloneable{

    private int age; // 定义年龄字段
    private Date birth; // 定义生日字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();	// 调用Object类的clone方法
    }

    public Person(int age, Date birth) {
        this.age = age;
        this.birth = birth;
    }

    public Person() {
    }
}

复制引用

对该类进行测试,

Date date = new Date();

Person p1 = new Person(23, date);
Person p2 = p1;
System.out.println(p1);		// 内存地址值
System.out.println(p2);		// 与上方地址相同

运行结果显示,两个对象的地址相同,是切切实实的同一个对象。这种形式称为复制引用。示意图如下,
image

复制对象

Date date = new Date();

Person p1 = new Person(23, date);
Person p2 = (Person) p1.clone();
System.out.println(p1);		// 地址1
System.out.println(p2);		// 地址2

运行结果显示,两个变量指向不同的内存地址,是两个不同的实例对象。具体示意图如下,
image

浅拷贝和深拷贝

进一步对调用clone方法得到的对象与源对象进行比较,比较对象中的成员变量。

Date date = new Date();

Person p1 = new Person(23, date);
Person p2 = (Person) p1.clone();
System.out.println(p1); // Person@1540e19d
System.out.println(p2); // Person@677327b6	复制对象

System.out.println(p1.getAge() == p2.getAge()); // true
date.setTime(234234234L);
System.out.println(p1.getBirth()); // Sun Jan 04 01:03:54 CST 1970
System.out.println(p2.getBirth()); // Sun Jan 04 01:03:54 CST 1970
System.out.println(p1.getBirth() == p2.getBirth()); // true

使用 clone 克隆的对象,其中 age 属于基础类型,而 date 属于引用类型。在经历了复制对象后,

  • 基础类型数据直接复制时就是值的传递

  • 当改变 date 对象导致 p1 和 p2 的 成员变量birth 都发生了变化,所以 p1 和 p2 的 birth 实际指向的还是同一个 Date 对象。

基本类型和引用型变量的值传递参考博文

针对引用类型变量,对其拷贝一般有两种,

  1. 浅拷贝:直接将原对象成员变量的引用赋给新的对象中对应的成员变量,这样两个对象的成员指向的是同一片内存地址。具体示意图如下,
    image
  2. 深拷贝:将原对象中引用型成员变量指向的对象复制一份,创建一个相同的对象。将这个新的对象的内存地址赋给新对象,这样两个内存地址不同的对象其中的成员变量也分别指向了两个不同的对象。示意图如下,
    image

2.代码实现

简单深拷贝

对之前的Person类的 clone 方法进行修改,

public class Person implements Cloneable{

    private int age; // 定义年龄字段
    private Date birth; // 定义生日字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        p.birth = (Date) birth.clone();	// 对象本身以及引用型成员变量进行复制对象操作
        return p;
    }

    public Person(int age, Date birth) {
        this.age = age;
        this.birth = birth;
    }

    public Person() {
    }
    //...省略get、set
}

对上方代码进行测试,

Date date = new Date();
Person p1 = new Person(23, date);
Person p2 = (Person) p1.clone();

System.out.println(p1 == p2);

date.setTime(234234234L);
System.out.println(p1.getBirth()); // Sun Jan 04 01:03:54 CST 1970
System.out.println(p2.getBirth()); // Wed Jun 26 19:43:15 CST 2019
System.out.println(p1.getBirth() == p2.getBirth()); // false

可以看到,修改 p1 对象的 birth 属性,但是 p2 中的 birth 并没有发生变化,这就是所谓的 “深拷贝”。

是否做到完全深拷贝

重写 clone 后实现了引用类型的深拷贝,但是,如果引用类型内部还存在引用型属性的话,那么拷贝后的对象是否实现了这种深层的拷贝?

public class Person implements Cloneable{

    private int age; // 定义年龄字段
    private Date birth; // 定义生日字段

    private Address address;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        p.birth = (Date) birth.clone();
        p.address = (Address) address.clone();
        return p;
    }

    public Person(int age, Date birth, Address address) {
        this.age = age;
        this.birth = birth;
        this.address = address;
    }

    public Person(int age, Date birth) {
        this.age = age;
        this.birth = birth;
    }

    public Person() {
    }

    //...省略get、set
}

上述代码中Address类的实现如下,

public class Address implements Cloneable{
    private Code code; // 地址的编号信息字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public Code getCode() {
        return code;
    }

    public void setCode(Code code) {
        this.code = code;
    }
}


public class Code {...}

对上面代码进行测试,

Date date = new Date();

Address address = new Address();
Person p1 = new Person(23, date, address);
Person p2 = (Person) p1.clone();

System.out.println(p1 == p2); // false

System.out.println(p1.getAddress()); // Address@1540e19d
System.out.println(p2.getAddress()); // Address@677327b6

System.out.println(p1.getAddress().getCode() == p2.getAddress().getCode()); // true

虽然 p1 和 p2 的 address 引用的对象已经区分开来了,但是这两对象的 code 属性还是指向的同一个 Code 对象。使用图示说明如下,
image

真正深拷贝

要实现 “深拷贝” 就需要单独拷贝某个属性来实现,这样修改后的 AddressCode 类如下,

public class Address implements Cloneable{
    private Code code; // 地址的编号信息字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Address address = (Address) super.clone();
        address.code = (Code) code.clone();
        return address;
    }

    public Address(Code code) {
        this.code = code;
    }
    ...省略get、set
}


public class Code implements Cloneable{

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

对上方代码进行测试,

Date date = new Date();

Address address = new Address(new Code());
Person p1 = new Person(23, date, address);
Person p2 = (Person) p1.clone();

System.out.println(p1 == p2); // false

System.out.println(p1.getAddress()); // Address@1540e19d
System.out.println(p2.getAddress()); // Address@677327b6

System.out.println(p1.getAddress().getCode() == p2.getAddress().getCode()); // false

image

3.通过序列化和反序列化实现深拷贝

概念 说明
序列化 将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象
反序列化 从字节流创建对象的相反的过程。而创建的字节流是与平台无关的,在一个平台上序列化的对象可以在不同的平台上反序列化。

需要为 Person、Address、Code 类实现 Serializable 接口,测试如下,

Date date = new Date();

Address address = new Address(new Code());
Person p1 = new Person(23, date, address);

// 首先将p1序列化存储,转为字节流形式
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(p1);

// 反序列化来实现p1的拷贝
byte[] bytes = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
Person p2 = (Person)ois.readObject();  //克隆的对象

System.out.println(p1 == p2); // false
System.out.println(p1.getAddress() == p2.getAddress()); // false
System.out.println(p1.getAddress().getCode() == p2.getAddress().getCode()); // false

4.使用场景

原型模式一般很少单独出现,一般都是和工厂方法模式一起搭配使用,通过 clone 来创建新的对象,然后由工厂方法返回。

创建对象的时候尽量不要依赖具体的对象类型,原型模式就很好的印证了这句话,避免僵硬地使用 new 来进行对象创建。

5.优缺点

优点

  1. 隐藏实例生成细节
  2. 一些场合下,复制对象比新建对象更有效
  3. 通过克隆而不是工厂方法产生对象

缺点

  1. 复制时比较复杂,特别是对象中引用层级很深的时候
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!