提示:标题序号从3开始,是照应不同设计模式笔记发布的顺序而定的,比如,第上一篇文章 ​​初学Java常用设计模式之——工厂模式​​ 序号从2开始。

标题后面之所以加上了解,是因为相对于单例模式,工厂模式来说原型模式用的比较少,但原型模式的深拷贝和浅拷贝是需要了解一下的!

3. 原型模式(了解)

3.1 原型模式介绍


  • 原型设计模式Prototype

    • 是一种对象创建型模式,使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,主要用于创建重复的对象,同时又能保证性能
    • 工作原理是将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程
    • 应该是最简单的设计模式了,实现一个接口,重写一个方法即完成了原型模式

  • 核心组成

    • Prototype: 声明克隆方法的接口,是所有具体原型类的公共父类,Cloneable接口
    • ConcretePrototype : 具体原型类
    • Client: 让一个原型对象克隆自身从而创建一个新的对象

  • 应用场景

    • 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得
    • 如果系统要保存对象的状态,做备份使用


3.2 原型模式案例

首先我们来创建一个具体原型类 Person.java 并让其实现 Cloneable 接口,重写clone() 方法:

/**
* @Auther: csp1999
* @Date: 2020/11/08/7:31
* @Description: Person 具体原型类实现 Cloneable 接口,能被克隆
*/
public class Person implements Cloneable{

private String name;
private int age;

public Person(){
System.out.println("空参构造函数调用...");
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

/**
* 重写克隆方法,返回Person对象类型
*
* 注意:权限改成public,方便调用
* @return
* @throws CloneNotSupportedException
*/
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}

在测试类中调用并打印结果:

@Test
public void testPropotype() throws CloneNotSupportedException {
Person person1 = new Person();
person1.setAge(22);
person1.setName("csp");
System.out.println(person1);
Person person2 = person1.clone();
person2.setName("hzw");
System.out.println(person2);
}

结果如下:

空参构造函数调用...
Person{name='csp', age=22}
Person{name='hzw', age=22}

从结果可以看出:person2 是 person1通过复制后得来的,二者数据内容相同。但需要注意的是,person1调用​​clone();​​方法得到person2,并没有经过Person 类中的空参构造函数,因此打印结果只输出一次​​空参构造函数调用...​​。

接下来,我们在Person 在加上新的复杂数据类型的成员变量:List

private List<String> list;

再来测试:

@Test
public void testPropotype() throws CloneNotSupportedException {
Person person1 = new Person();
person1.setAge(22);
person1.setName("csp");
// 初始化list 并为其加入数据
person1.setList(new ArrayList<>());
person1.getList().add("aaa");
person1.getList().add("bbb");
System.out.println("person1:"+person1);
Person person2 = person1.clone();
person2.setName("hzw");
// 给peron2 中的list添加一条数据
person2.getList().add("ccc");
System.out.println("person2"+person2);
System.out.println("person1:"+person1);
boolean flag1 = person1 == person2;
System.out.println("person1 和 person2 的 引用地址是否相同: " + flag1);
boolean flag2 = person1.getList() == person2.getList();
System.out.println("person1 和 person2 的 list 引用地址是否相同: " + flag2);
}

输出结果:

空参构造函数调用...
person1:Person{name='csp', age=22, list=[aaa, bbb]}
person2Person{name='hzw', age=22, list=[aaa, bbb, ccc]}
person1:Person{name='csp', age=22, list=[aaa, bbb, ccc]}
person1 和 person2 的 引用地址是否相同: false
person1 和 person2 的 list 引用地址是否相同: true

由结果可以看出:


  • 当克隆执行完成后,实际上相当于新 new 一个Person 对象并为其分配了新的存储地址及引用,因此person1 和 person2 的地址引用不同;
  • 而对于Person 对象的复杂类型成员变量 list,当执行克隆的时候,实际上是从被拷贝对象​​person1​​​ 中 拷贝了list 的引用地址给​​person2​​ 中的 list,而并非新new(创建)一个 List 出来;
  • 因此二者其实是共享一个相同地址引用的list,所以​​person1.getList() == person2.getList();​​ 为true,这也就说明了,为什么在 ​​person2​​​的list 中添加数据​​ccc​​​时,​​person1​​​ 中的list也添加了​​ccc​​,而这种情况就被称为 浅拷贝;

那么如何解决浅拷贝的问题呢?请接着往下阅读!

3.1 原型模式深拷贝/浅拷贝


  • 遗留问题:

    • 通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的
    • 浅拷贝实现 Cloneable,深拷贝是通过实现 Serializable 读取二进制流
    • 拓展

      • 浅拷贝:
        ​如果原型对象的成员变量是基本数据类型(int、double、byte、boolean、char等),将复制一份给克隆对象; 如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象, 也就是说原型对象和克隆对象的成员变量指向相同的内存地址 通过覆盖Object类的clone()方法可以实现浅克隆 ​
      • 深拷贝:
        ​无论原型对象的成员变量是基本数据类型还是引用类型,都将复制一份给克隆对象,如果需要实现深克隆,可以通过序列化(Serializable)等方式来实现 ​


  • 优点

    • 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,可以提高新实例的创建效率
    • 可辅助实现撤销操作,使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用恢复到历史状态

  • 缺点

    • 需要为每一个类配备一个克隆方法,对已有的类进行改造时,需要修改源代码,违背了“开闭原则”
    • 在实现深克隆时需要编写较为复杂的代码,且当对象之间存在多重的嵌套引用时,需要对每一层对象对应的类都必须支持深克隆


深拷贝实现:

首先Person 对象实现 Serializable 接口,然后自定义深拷贝方法 deepClone()

/**
* 深拷贝
*
* 注意:要实现序列化接口
* @return
*/
public Person deepClone() {
try {
// 输出 (序列化)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
// 输入 (反序列化)
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Person person = (Person) ois.readObject();
return person;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

接下来验证一下深拷贝是否成功:

@Test
public void testPropotype() throws CloneNotSupportedException {
Person person1 = new Person();
person1.setAge(22);
person1.setName("csp");
// 初始化list 并为其加入数据
person1.setList(new ArrayList<>());
person1.getList().add("aaa");
person1.getList().add("bbb");
System.out.println("person1:"+person1);
//-----------------------------浅拷贝-------------------------------
//Person person2 = person1.clone();
//-----------------------------深拷贝-------------------------------
Person person2 = person1.deepClone();
person2.setName("hzw");
// 给peron2 中的list添加一条数据
person2.getList().add("ccc");
System.out.println("person2"+person2);
System.out.println("person1:"+person1);
boolean flag1 = person1 == person2;
System.out.println("person1 和 person2 的 引用地址是否相同: " + flag1);
boolean flag2 = person1.getList() == person2.getList();
System.out.println("person1 和 person2 的 list 引用地址是否相同: " + flag2);
}

输出结果:

空参构造函数调用...
person1:Person{name='csp', age=22, list=[aaa, bbb]}
person2Person{name='hzw', age=22, list=[aaa, bbb, ccc]}
person1:Person{name='csp', age=22, list=[aaa, bbb]}
person1 和 person2 的 引用地址是否相同: false
person1 和 person2 的 list 引用地址是否相同: false

由结果可得出:深拷贝 ​​person2​​​ 所得到的 list 内存地址和原来​​person1​​ 中的内存地址是不同的,深拷贝成功!