原型模式: Prototype Module

顾名思义,这个模式有一个样板实例,用户从这个样板对象中复制出一个内部属性一致的对象,这个过程也就是我们俗称的克隆。被复制的实例就被成为“原型”。

#定义

用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象

##使用场景

1. 类初始化需要消耗非常多的资源,包括数据,硬件资源,通过原型拷贝可以避免这些消耗
2. 通过new产生一个对象需要非常繁琐的数据准备或访问权限
3. 一个对象需要提供给其他对象访问,并且每个调用者可能都需要修改其值时,可以通过保护性原型拷贝拷贝多个对象供调用者使用。

注意
通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比通过new操作速度快,只有当通过new构造对象较为耗时或者是成本较高时,通过clone方法才能获得效率上的提升。

当然,原型模式除了实现Cloneable接口来实现,同样也存在这其他的实现方式.

#实现

##方法一: 实现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
42
43
44
45
import java.awt.List;
import java.util.ArrayList;

public class WordDocument implements Cloneable {

private String mText;
private ArrayList<String> mImages = new ArrayList<>();

public WordDocument() {
System.out.println("执行构造函数");
}

public ArrayList<String> getImages(){
return mImages;
}

public void addImages(String img){
mImages.add(img);
}

public String getText() {
return mText;
}

public void setText(String mText) {
this.mText = mText;
}

public void showDocument() {
System.out.println("-------------文本内容----------");
System.out.println("Text : " + mText);
for (String string : mImages) {
System.out.println("image name : " + string);
}
System.out.println("-------------内容结束----------");
System.out.println("");
}

@Override
protected WordDocument clone() throws CloneNotSupportedException {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
return doc;
}
}
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
public class PrototypeModule {

public static void main(String[] args) {
WordDocument document = new WordDocument();
document.setText("初始文本信息");
document.addImages("img1");
document.addImages("img2");
document.addImages("img3");
document.showDocument();

try {
WordDocument documentClone = document.clone();
System.out.println("修改text");
documentClone.setText(" + 修改文本信息");
documentClone.addImages("增加图片");
documentClone.showDocument();

System.out.println("增加图片");
document.showDocument();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}

}
}

结果
从结果中可以看出,当我们在被拷贝出来的对象中修改ArrayList中的数据时,原对象的ArrayList数据也改变了,这是因为我们这里使用的方式只是一个浅拷贝,它其实并不是将原始文档的所有字段都重新构造了一份,而是直接将副本文档的字段直接引用原始文档的字段,因此当我们改变副本文档时,原始文档也随着改变了。

注意
为什么我们也修改了text,但是原始文档的值还是没有变化?
String类型是一个值类型
String指向的对象是一个不可变的对象,String类型的每次变化其实都是创建一个新的对象,之后再将引用指向这个新的对象而已。之前的那个对象其实还是存在的,只是我们不指向它了。
如果将String mText更改为StringBuffer mText, setText方法更改为append(text)的话结果就一样了。

深拷贝
-
鉴于上面的浅拷贝存在的一些问题,我们可以采用深层拷贝来解决它。之所以会出现修改副本时原本会改变,是因为副本是直接指向原本的引用的。因此,我们只需要在拷贝对象时,对于引用型的字段也采用拷贝的形式,而不是单纯的引用。

修改如下:

1
2
3
4
5
6
7
@Override
protected WordDocument clone() throws CloneNotSupportedException {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
doc.mImages = (ArrayList<String>) this.mImages.clone();
return doc;
}

这样的话副本的引用型字段就指向原本字段的拷贝了,而不是它的本身,这样就不会引起连锁反应。
而上面的text由于是String类型,是一个值类型,不是引用类型

实际使用
-
在Android中,Intent也是使用了一个原型模式。而Intent为什么要使用原型模式呢?就是因为这个Intent和以前的一个Intent在很多地方是一样的,我们只需要更改它的部分信息就可以了,因此先拷贝,再修改,节约资源。而在Intent中,存在一段代码,这段代码也很好的解释了之前总结的那句话:clone方法是要看情况的,需要根据构造对象的成本来决定是clone还是new一个新的。

1
2
3
4
@Override
public Object clone(){
return new Intent(this);
}

这里并没有使用super.clone()方法来实现对象的拷贝,而是直接new的一个新的,因为考虑到成本的问题

在Bundle以及OkHttp中也是使用了原型模式,其余使用的地方自己可以多看看,多找找。

—————————-华丽丽的分割线——————————

在这里记录一个自己用到的地方吧:
在进行登陆操作时,会通过一个session来保存用户的登录信息,而这些信息会在APP模块中调用,比如说登陆失效的判断等等。在登陆时,会先获取一次之前的登录信息,登陆完成后就要从网络上获取一个新的信息,例如登陆的时间存放到session中。
而这个时候就可以使用原型模式了,深层拷贝的方法:
第一次登陆时使用拷贝出来的副本进行登陆,等网络请求结束,成功获取到当前登陆的服务器时间时就修改原本中的用户信息。

#总结

原型模式使用起来还是挺好的,而具体什么时候使用也是看项目的需求吧,看什么合适就使用什么模式