【设计模式】好玩的原型模式:Prototype
作者:行百里er
博客:https://chendapeng.cn (opens new window)
提示
这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!
# 原型模式
原型(Prototype)模式的定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
比如,用过VMware安装过虚拟机的可能知道,我们可以先安装一个模板机,然后通过克隆模板机创建出很多虚拟机出来,这种采用复制的方法大大提升了效率。
再比如,群发消息的场景,我们希望群发出去的东西title随着发送对象的不同而改变,这时可以构造出一个消息对象,群发复制这个对象,然后title进行个性化定制。
用消息作为原型对象,具体对象来自于拷贝原对象,要完成对象的拷贝,原型类必须实现Cloneable
接口,类图如下所示:
# 浅克隆
原型模式的克隆有 浅克隆
和 深克隆
,我们通过例子来演示一下。
假设Message类有String类型的title属性、int类型的state属性以及Contact引用类型的contact属性,类图:
public class Message implements Cloneable {
private String title;
private int state;
private Contact contact;
public Message(String title, int state, Contact contact) {
this.title = title;
this.state = state;
this.contact = contact;
}
@Override
public String toString() {
return "Message{" +
"title='" + title + '\'' +
", state=" + state +
", contact=" + contact +
'}';
}
public static void main(String[] args) {
try {
Contact contact = new Contact("abc@1.com", "666");
Message msg1 = new Message("张三", 1, contact);
Message msg2 = (Message) msg1.clone();
System.out.println("msg1:" + msg1);
System.out.println("msg2:" + msg2);
System.out.println("contact1 == contact2 ? " + (msg1.contact == msg2.contact));
msg1.contact.setPhone("888");
System.out.println("msg1:" + msg1);
System.out.println("msg2:" + msg2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Contact {
private String email;
private String phone;
public Contact(String email, String phone) {
this.email = email;
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "Contact{" +
"email='" + email + '\'' +
", phone='" + phone + '\'' +
'}';
}
}
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
61
62
63
64
65
66
67
68
69
70
71
72
运行结果:
msg1:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='666'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='666'}}
contact1 == contact2 ? true
msg1:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='888'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='888'}}
2
3
4
5
从结果可以看到,对象msg1的contact和msg2的contact竟然相等,也就是执行了同一个内存地址,而且,修改了msg1的contact属性,msg2的也跟着变了!
这种就属于浅克隆,创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
再来变一下,msg2克隆出来后,把它的title重新设置一下看看:
Contact contact = new Contact("abc@1.com", "666");
Message msg1 = new Message("张三", 1, contact);
Message msg2 = (Message) msg1.clone();
//重新设置msg1的title
msg1.title = "李四";
System.out.println("msg1:" + msg1);
System.out.println("msg2:" + msg2);
System.out.println("contact1 == contact2 ? " + (msg1.contact == msg2.contact));
msg1.contact.setPhone("888");
System.out.println("msg1:" + msg1);
System.out.println("msg2:" + msg2);
2
3
4
5
6
7
8
9
10
11
12
13
结果:
msg1:Message{title='李四', state=1, contact=Contact{email='abc@1.com', phone='666'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='666'}}
contact1 == contact2 ? true
msg1:Message{title='李四', state=1, contact=Contact{email='abc@1.com', phone='888'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='888'}}
2
3
4
5
msg1和msg2的title就不一样了!
疑问来了,String
也不是基本类型啊,msg1的title变了,为什么它就和我自定义的引用类型Contact不一样完全拷贝到msg2呢?
对于String类型,Java就希望你把它 认为是基本类型,它是没有clone方法的,处理机制也比较特殊,通过字符串常量池(stringpool)在需要的时候才在内存中创建新的字符串。
当定义完msg1,并根据msg1拷贝出msg2后,两个title都执行String常量池中的张三
,而msg1.title="李四"
执行后,msg1就指向了常量池中的李四
;对于Contact类型,这是一个自定义的引用,在内存中就是那个地址,因此,msg1.contact==msg2.contact,即执行同一个内存地址!
# 深克隆
很多时候,我们当然不希望浅克隆,比如上面的案例中,每个msg对象的contact都不应该是一样的,这就需要深克隆的存在了。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
那么就让Contact类也实现Cloneable接口,同时Message类的clone方法要对引用也克隆一份:
public class Message implements Cloneable {
private String title;
private int state;
private Contact contact;
@Override
protected Object clone() throws CloneNotSupportedException {
Message msg = (Message) super.clone();
//将引用对象也克隆一份
msg.contact = (Contact) contact.clone();
return msg;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
class Contact implements Cloneable {
//...
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
//...
}
2
3
4
5
6
7
8
9
10
执行如下代码:
Contact contact = new Contact("abc@1.com", "666");
Message msg1 = new Message("张三", 1, contact);
Message msg2 = (Message) msg1.clone();
System.out.println("contact1 == contact2 ? " + (msg1.contact == msg2.contact));
msg1.contact.setPhone("888");
System.out.println("msg1:" + msg1);
System.out.println("msg2:" + msg2);
2
3
4
5
6
7
8
9
结果:
contact1 == contact2 ? false
msg1:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='888'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='666'}}
2
3
contact1 == contact2为false了,因为它们指向了不同的内存地址了,此时修改msg1的contact属性,msg2随之而改变。
# Prototype模式应用场景
原型模式通常适用于以下场景。
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
- 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
- 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
- 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。
在Spring中,如果一个类被标记为prototype
,每一次请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)都会产生一个新的bean实例。
# 小结
- Java自带原型模式,Object类提供了clone方法
- 要实现原型模式必须实现Cloneable接口
- 重写clone方法,如果之重写clone,而未实现Cloneable接口,调用时会出现异常
- 该模式用于对一个对象的属性已确定,需产生很多相同对象的时候
- 注意区分深克隆与浅克隆
首发公众号 行百里er ,欢迎老铁们关注阅读指正。