image.png

通过 new 关键字生成实例是需要指定类名的。在开发过程中,我们需要「在不指定类名的前提下生成实例」的需求。

在以下情况,我们就不能根据类来生成实例,而要根据现有的实例来生成新的实例。

  1. 对象种类繁多,无法将它们整合到一个类中。需要处理的对象太多,如果将它们分别作为一个类,必须要编写很多个类文件。
  2. 难以根据类生成实例。生成实例的过程太复杂。
  3. 想解耦框架与生成的实例时。想让生成实例的框架不依赖于具体的类。

Prototype 模式:

  • 在软件系统中,有时候需要多次创建某一类型的对象,为了简化创建过程,可以只创建一个对象,然后再通过克隆的方式复制出多个相同的对象
  • 原型模式不跟据类生成实例,而是根据实例来生成新实例。在 Java 中,可以使用 clone 创建出实例的副本。
  • 原型模式在创建重复对象的同时,需要保证性能。可以通过 clone() 拷贝已有对象的数据,更新少量差值来实现。

缺点:

  • 需要为每一个类配备一个克隆方法,因此对已有类进行改造比较麻烦。需要修改其源代码,并且在实现深克隆时需要编写较为复杂的代码。

原型模式的基本工作原理:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象复制原型自己来实现创建过程。

登场角色:

  • Prototype(抽象原型类):定义用于复制现有实例来生成新实例的方法,可以是抽象类也可以是接口。在 Java 中,这个抽象类是实现了 @Cloneable 接口的。
  • ConcretePrototype(具体原型类):实现复制现有实例并生成新实例的方法。
  • Client(使用者):使用复制实例的方法生成新实例。
classDiagram
    class Client {
    }

    class Prototype {
        <<abstract>>
        +clone() Prototype
    }

    class ConcretePrototypeA {
        +clone() Prototype
    }

    class ConcretePrototypeB {
        +clone() Prototype
    }

    Client --> Prototype : uses
    Prototype <|-- ConcretePrototypeA
    Prototype <|-- ConcretePrototypeB

相关的设计模式:

  • 享元模式 Flyweight:使用 Prototype 模式可以生成一个与当前实例的状态完全相同的实例。而使用 Flyweight 模式可以在不同的地方使用同一个实例。
  • Memento 模式:使用 Prototype 模式可以生成一个与当前实例的状态完全相同的实例。而使用 Memento 模式可以保存当前实例的状态,以实现快照和撤销功能。
  • Composite 模式 以及 装饰者模式 Decorator:经常使用 Composite 模式和 Decorator 模式时,需要能够动态地创建复杂结构的实例。这时可以使用 Prototype 模式,以帮助我们方便地生成实例。
  • Command 模式:想要复制 Command 模式中出现的命令时,可以使用 Prototype 模式。

示例:带原型管理器的原型模式

《图解设计模式》第六章中的案例其实就是使用了原型管理器。

原型管理器(Prototype Manager)角色创建具体原型类的对象,并记录每一个被创建的对象。原型管理器的作用与工厂相似,其中定义了一个集合用于存储原型对象,如果需要某个对象的一个克隆,可以通过复制集合中对应的原型对象来获得。在原型管理器中针对抽象原型类进行编程,以便扩展。

案例 1:Color

image.png

抽象原型类 MyColor

1
2
3
4
public interface MyColor extends Cloneable {
public Object clone();
public void display();
}

具体原型类 Red

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Red implements MyColor {
@Override
public Object clone() {
Red r=null;
try {
r = (Red) super.clone();
}catch (Exception e){}
return r;
}

@Override
public void display() {
System.out.println("This is Red");
}
}

具体原型类 Blue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Blue implements MyColor {
@Override
public Object clone() {
Blue b=null;
try {
b = (Blue) super.clone();
}catch (Exception e){

}
return b;
}
@Override
public void display() {
System.out.println("This is Blue");
}
}

原型管理器类 PrototypeManager

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PrototypeManager {
private Hashtable ht =new Hashtable();
public PrototypeManager() {
ht.put("red",new Red());
ht.put("blue",new Blue());
}
public void addColor(String key,MyColor obj){
ht.put(key,obj);
}
public MyColor getColor(String key){
return (MyColor) ((MyColor)ht.get(key)).clone();
}
}

客户端测试类 Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {

public static void main(String[] args) {
PrototypeManager pm =new PrototypeManager();

MyColor obj1=pm.getColor("red");
obj1.display();

MyColor obj2=pm.getColor("red");
obj2.display();

System.out.println(obj1==obj2); // false

}
}

案例 2:Shape

image.png

引用拷贝和对象拷贝

对象和引用的区别:

  • 对象:绝大多数对象在堆区,它是实际保存属性的内存空间
  • 引用:引用大多引用在栈区,可以将它理解为指向实际对象地址的指针

一个对象可以有多个引用,但一个引用只能指向一个对象。当我们使用 == 比较对象时,一般比较的是对象地址。

引用拷贝

引用拷贝示例:

1
2
3
4
Teacher teacher = new Teacher("Taylor",26);
Teacher otherteacher = teacher;
System.out.println(teacher); // blog.Teacher@355da254
System.out.println(otherteacher); // blog.Teacher@355da254

image.png

引用拷贝通常发生在传递参数、返回值等场景中。例如,在 Java 中,如果将一个对象作为参数传递给方法,实际上是将该对象的引用传递给了方法,而不是对象本身的拷贝。

对象拷贝

对象拷贝:创建对象本身的一个副本。

1
2
3
4
Teacher teacher = new Teacher("Swift",26); 
Teacher otherteacher = (Teacher)teacher.clone();
System.out.println(teacher); // blog.Teacher@355da254
System.out.println(otherteacher); // blog.Teacher@4dc63996

image.png

深拷贝与浅拷贝

深拷贝和浅拷贝都是对象拷贝。它们之间的主要区别在于是否复制了对象内部的数据。

  • 浅拷贝 Shallow Copy:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
  • 深拷贝 Deep Copy:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象,换言之,深复制把要复制的对象所引用的对象都复制了一遍。

🍐⚱️:(谈浅拷贝)你用魔法棒复制了自己,但是复制人和你都用着同样一台电脑)

image.png

在上图的例子中,单纯使用 clone() 方法得到的是浅克隆的结果。

Java Object 类中的 clone() 方法

clone 用于复制实例。clone 方法定义在 java.lang.Object 中,Object 又是所有类的父类,也就是说所有的 Java 类都继承了 clone 方法。但是,继承了该方法并不意味着就能直接用。能调用 clone 方法的前提是这个类必须实现了 Cloneable 接口。

Object 类中的 clone() 方法是通过 native 修饰的,也就是说它是通过直接调用底层操作系统方法实现的。当没有重写该方法时就默认调用这个方法。

clone()new 一样,都可以创建一个新的对象。

  • new:当我们使用 new 关键字创建对象时,JVM 首先根据关键字后面的类型确定需要申请的内存大小,申请完内存后,执行类的构造方法。在执行构造方法期间,填充内存中各个属性域,这个填充的过程也叫初始化。构造方法执行完标志着对象创建成功,此时返回对象地址,在栈区以引用的方式调用对象。(当然,由于指令的重排序,发布对象可能在构造函数返回之前)
  • clone():当我们调用某个对象的 clone() 方法克隆对象时,首先根据原对象的内存大小申请内存空间,申请完内存空间后,将原对象内存域复制到新申请的内存空间,复制完成标志着克隆完成,返回引用类型。也就是说,clone() 的过程没有调用类的构造函数

简单总结:newclone() 第一步都是申请内存,只不过 new 关键字通过类构造方法初始化对象,clone() 方法直接通过克隆内存域完成对象创建。

clone() 是深克隆还是浅克隆?

  • 如果一个对象中所有属性都是基础类型(intboolean),那么它的深克隆和单纯 clone() 的浅克隆结果完全相同。
  • 如果一个对象包含引用类型数据,如果克隆之后的引用所指向对象是不同对象(看你 clone() 的具体实现),那么它是深克隆,否则是浅克隆。

深拷贝的实现

手动赋值、第三方库也可以实现深拷贝。

Cloneable:重写 clone 方法

在编码过程中,一般都是通过实现 java.lang.Cloneable 接口重写 clone() 方法,然后就可以调用重写后的 clone() 方法执行自己的逻辑。Cloneable 接口中并没有声明任何方法。他只是用来标记哪个类可以进行复制。这种接口称为「标记接口」。

Java 要求被克隆的类必须显式实现 Cloneable 接口。如果没有实现 Cloneable 接口的类的实例调用了 clone 方法, 则会在运行时抛出 CloneNotSupportedException 异常。

在实际应用开发中,浅克隆肯定不能满足所有业务场景。部分情况下,需要将浅克隆优化为深克隆,具体实现方法:实现 Cloneable 接口,重写 clone() 方法,在 clone() 方法中手动克隆引用属性。重写时记得 super.clone()

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Test {
static class Body implements Cloneable {
public Head head;

public Body() {}

public Body(Head head) {
this.head = head;
}

@Override
protected Object clone() throws CloneNotSupportedException {
Body newBody = (Body) super.clone();
newBody.head = (Head) head.clone();
return newBody;
}
}

static class Head implements Cloneable {
public Face face;

public Head() {}

public Head(Face face) {
this.face = face;
}

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

static class Face implements Cloneable {
public Face() {
}
}

public static void main(String[] args) throws CloneNotSupportedException {
Body body = new Body(new Head());
Body body1 = (Body) body.clone();
System.out.println("body == body1 : " + (body == body1)); // false
System.out.println("body.head == body1.head : " + (body.head == body1.head)); // false
}
}

实际上,上面的代码克隆得还是不够彻底:

image.png

想要做到完完整整的深克隆,必须保证所有引用属性克隆后都会创建新对象,并且这个过程需要无限向下递归,直到只剩下常量属性。想要实现这种程度的深克隆几乎是不可能的,因为一旦代码中引入 SDK 包中的类,且该类没有重写 clone() 方法,就无法实现深克隆。

Serializable:序列化后再反序列化

把对象写到流里的过程是串行化(Serilization)过程,一种形象的说法为「冷冻」或者「腌咸菜(picking)」过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做「解冻」或者「回鲜(depicking)」过程。

将对象序列化,然后再反序列化成新的对象是深拷贝的一种方法。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package top.uuanqin;

import java.io.*;
import java.util.Date;

public class TestClone {
public static class User implements Serializable {
private int id;
private String username;// 用户姓名
private String sex;// 性别
private Date birthday;// 生日
private String address;// 地址
private Person person; //引用类型

public User myColon() {
User copy = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
//将流序列化成对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
copy = (User) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return copy;
}

//此处省略get-set方法代码
}

// 引用类型也需要实现Serializable接口,否则会序列化失败。
public static class Person implements Serializable {
private int id;
private String userName;
private int age;
private String mobilePhone;

public Person() {
}

public Person(int id, String userName, int age, String mobilePhone) {
this.id = id;
this.userName = userName;
this.age = age;
this.mobilePhone = mobilePhone;
}
//此处省略get-set方法
}

public static void main(String[] args) {
User u1 = new User();
u1.person = new Person(); // 让引用对象不为空
User u2 = u1.myColon();
System.out.println(u1 == u2); // false
System.out.println(u1.person == u2.person); // false
}
}

使用反射方法

通过反射生成对象,通过反射机制获取该对象的所有字段和属性信息。遍历所有字段和属性,以递归方式将源对象中的值复制到目标对象中。

本文参考