1. 概述

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

2. 结构

原型模式包含如下角色:

  • 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
  • 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
  • 访问类:使用具体原型类中的 clone() 方法来复制新的对象。

接口类图如下:

image-20230112212928339

3. 实现

原型模式的克隆分为浅克隆和深克隆。

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,比如某个类的对象或者数组对象,仍指向原有属性所指向的对象的内存地址。%%
0853-🏡⭐️◼️浅克隆中非基本数据类型的对象包括什么?普通对象和数组对象◼️⭐️-point-202301270853%%

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

Java 中的 Object 类中提供了 clone() 方法来实现浅克隆。 Cloneable 接口是上面的类图中的抽象原型类,而实现了 Cloneable 接口的子实现类就是具体的原型类。代码如下:

Realizetype(具体的原型类):

1
2
3
4
5
6
7
8
9
10
11
12
public class Realizetype implements Cloneable {

public Realizetype() {
System.out.println("具体的原型对象创建完成!");
}

@Override
protected Realizetype clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return (Realizetype) super.clone();
}
}

PrototypeTest(测试访问类):

1
2
3
4
5
6
7
8
public class PrototypeTest {
public static void main(String[] args) throws CloneNotSupportedException {
Realizetype r1 = new Realizetype();
Realizetype r2 = r1.clone();

System.out.println("对象r1和r2是同一个对象?" + (r1 == r2));
}
}

4. 注意事项

  1. 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
  2. 不用重新初始化对象,而是动态地获得对象运行时的状态
  3. 如果原始对象发生变化 (增加或者减少属性),克隆出来的克隆对象也会发生相应的变化,无需修改代码。如果是使用默认克隆方法,即浅克隆,修改原始对象的非基本数据类型属性会影响其他克隆对象的对应属性,因为浅克隆最终指向的是同一个对象。
  4. 在实现深克隆的时候可能需要比较复杂的代码
  5. 缺点:要实现原型模式需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了 ocp 原则。❕%%
    1037-🏡⭐️◼️使用 cloneable 接口存在违反 OCP 原则的问题◼️⭐️-point-202301231037%%

5. clone 与 new 的区别⭐️🔴

  1. new 操作符本意是在堆中开辟内存空间。程序执行到 new 时,会根据 new 后面的类型来计算分配的内存空间大小,分配完空间以后,调用构造函数填充对象的各个域,然后返回该对象的地址。具体流程可参考 对象创建-7、JVM-堆
  2. 在调用 clone 方法时,程序会分配一块与原对象相同大小的内存空间,然后用原对象的各个域填充到新对象相应的各个域中,然后返回新对象的地址。
  3. 由于 native 方法通常比非 native 方法更加高效,所以 clone 方法效率更高

6. 浅拷贝 (浅克隆)

%%
▶4.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230604-2216%%
❕ ^42eo02

  1. 对于基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象
  2. 对于引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
  3. 只有可变对象的引用才会出现上述问题,不可变类(IMMUTABLE CLASS)的引用,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等,每次对不可变对象的修改都将产生一个新的不可变对象,因此无论修改 clone 出的对象的可变对象,还是修改原对象中的可变对象,都会导致可变对象的引用值发生变化,而不是可变对象本身发生变化。

7. 深拷贝 (深克隆)

  1. 复制对象的所有基本数据类型的成员变量值
  2. 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象进行拷贝

7.1. 实现方式

  1. 重写 clone 方法
    如果类比较多或者层次比较深,那么修改起来非常困难,不推荐,推荐使用序列化方式实现深克隆。而且违反 OCP 原则。
  2. 序列化
    通过对象序列化实现深拷贝

8. clone 的替代方案⭐️🔴

%%
▶82.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230306-2242%%
❕ ^ngb0a7

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

%%
▶79.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230306-1705%%
❕ ^zjymzy

  1. 拷贝构造器: public Yum(Yum yum);
  2. 拷贝工厂: public static Yum newInstance(Yum yum);
  3. 使用序列化

8.1. 拷贝工厂 (拷贝构造器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 复制构造函数  
public Student(Student student)
{
this.name = student.name;
this.age = student.age;

// 浅拷贝
// this.subjects = student.subjects;

// 深拷贝——创建一个新的 `HashSet` 实例
this.subjects = new HashSet<>(student.subjects);
}

// 复制工厂
public static Student newInstance(Student student) {
return new Student(student);
}

DesignPattern: [[OptimizationDeepCopy.java]]

9. 实战经验

10. 参考与感谢

设计模式-2、设计模式及设计原则

https://www.techiedelight.com/zh/copy-constructor-factory-method-java/