GeekIBLi

设计模式之美-原型模式

2021-08-07

原型模式

原型模式(Prototype Pattern)指原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象,属于创
建型模式;

原型模式的核心在于复制原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行复制,不需要
再精力耗时的对象初始化过程(不调用构造函数),性能提升很多。当对象的构造过程比较耗时时,可以把当前系统
已存在的对象作为原型,对其进行复制(一般是基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大缩短;

原型模式类图

IPrototype 定义克隆的方法 类似于JDK自带的Cloneable

1
2
3
public interface IPrototype<T> {
T clone();
}

ConcretePrototype 具体的要克隆的对象

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
public class ConcretePrototype implements IPrototype {

private int age;
private String name;

public int getAge() {
return age;
}

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

public String getName() {
return name;
}

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

// 可能我们平时都这么去复制对象
@Override
public ConcretePrototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(this.age);
concretePrototype.setName(this.name);
return concretePrototype;
}

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

Client 客户端 测试

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("Tom");
System.out.println(prototype);

//拷贝原型对象
ConcretePrototype cloneType = prototype.clone();
System.out.println(cloneType);
System.err.println(cloneType == prototype);
}

运行结果:

1
2
3
ConcretePrototype{age=18, name='Tom'}
ConcretePrototype{age=18, name='Tom'}
false

实现JDK Cloneable的克隆对象写法

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
public class ConcretePrototype1 implements Cloneable {

private int age;
private String name;

public int getAge() {
return age;
}

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

public String getName() {
return name;
}

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

@Override
public ConcretePrototype1 clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
System.err.println("Clone Error!");
}
return (ConcretePrototype1) clone;
}

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

输出结果同上;以上的两个属性都是基本数据类型和String,并没有引用类型,下面我们添加一个引用类型的属性测试以下👇👇

ConcretePrototype添加一个List类型属性

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
public class ConcretePrototype implements IPrototype {

private int age;
private String name;
private List<String> hobbies;

public List<String> getHobbies() {
return hobbies;
}

public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}

public int getAge() {
return age;
}

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

public String getName() {
return name;
}

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

@Override
public ConcretePrototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(this.age);
concretePrototype.setName(this.name);
concretePrototype.setHobbies(this.hobbies);
return concretePrototype;
}

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

SETTER方法实现浅克隆

浅克隆也叫浅拷贝:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
//创建原型对象
ConcretePrototype1 prototype = new ConcretePrototype1();
prototype.setAge(18);
prototype.setName("Tom");
ArrayList<String> strings = Lists.newArrayList("数学", "英语");
prototype.setHobbies(strings);
System.out.println(prototype);

//拷贝原型对象
ConcretePrototype1 cloneType = prototype.clone();
cloneType.getHobbies().add("语文");
System.out.println(cloneType);
System.out.println(prototype);
System.err.println(cloneType == prototype);
System.err.println(cloneType.getHobbies() == prototype.getHobbies());
}
输出结果:
ConcretePrototype1{age=18, name='Tom', hobbies=[数学, 英语]}
ConcretePrototype1{age=18, name='Tom', hobbies=[数学, 英语, 语文]}
ConcretePrototype1{age=18, name='Tom', hobbies=[数学, 英语, 语文]}
false
// 这里为什么结果是true看到上面画的图应该可以看懂吧,set方法其实是将list的引用设置过去,并不是创建一个新的list再赋值!
true

通过内存字节流”克隆”对象,实现深克隆

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

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
public class ConcretePrototypeDeep implements Cloneable,Serializable {

private int age;
private String name;
private List<String> hobbies;

public List<String> getHobbies() {
return hobbies;
}

public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}

public int getAge() {
return age;
}

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

public String getName() {
return name;
}

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

// 基于内存字节流生成对象
public ConcretePrototypeDeep deepClone() {
try {
//将当前对象信息写到内存
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
//从内存中读取对象信息,强制转换成当前类型
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (ConcretePrototypeDeep) objectInputStream.readObject();
} catch (IOException ioException) {
ioException.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}


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

输出结果:

1
2
3
4
5
ConcretePrototype{age=18, name='Tom', hobbies=[数学, 英语]}
ConcretePrototype{age=18, name='Tom', hobbies=[数学, 英语, 语文]}
ConcretePrototype{age=18, name='Tom', hobbies=[数学, 英语]}
false
false

在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。

要实现深拷贝,必须实现Cloneable接口,去重写clone方法,否则会抛出CloneNotSupportedException异常,
但是如果对象中包含很多引用类型的属性,这样去覆盖clone方法其实是很麻烦的,可以优先使用序列化的方式实现!

克隆破坏单例模式

如果我们克隆的目标的对象是单例对象,这便意味着,深克隆会破坏单例。解决以上问题的思路:

  • 禁止深克隆
  • 在单例对象的getInstance方法,返回当前对象,而不是去新创建一个对象或者通过内存字节流等方法生成对象

原型模式在Java中的应用

ArrayList底层是基于数组结果的,它的动态扩容过程是创建一个新的数组,并把数组中的元素拷贝过去,用新的数组来继续存放元素;
在创建新数组的过程中便使用了原型模式。

java.util.ArrayList.clone

1
2
3
4
5
6
7
8
9
10
11
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}

java.util.Arrays.copyOf(T[], int)

1
2
3
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}

java.util.Arrays.copyOf(U[], int, java.lang.Class<? extends T[]>)

1
2
3
4
5
6
7
8
9
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

最终还是System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));这个方法来完成数组的拷贝!
像我们使用的

com.alibaba.fastjson.JSON#parseObject

org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object)`

等都是原型模式;

原型模式适用场景

  • 类初始化消耗资源过多
  • new一个对象需要很多繁琐的过程(数据准备,访问权限等)
  • 构造函数比较复杂
  • 循环体内产生大量对象时

原型模式的优点

  • Java自带的原型模式是基于内存二进制流的复制,在性能上比直接创建一个对象更加优良
  • 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,
    以便在需要的时候使用,可辅助实现撤销操作。

原型模式的缺点

  • 需要为每一个类都配置一个 clone 方法
  • clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则
  • 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,
    实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当