原型模式
原型(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 + '\'' +
'}';
}
}
运行结果:
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'}}
从结果可以看到,对象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);
结果:
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'}}
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;
}
}
class Contact implements Cloneable {
//...
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
//...
}
执行如下代码:
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);
结果:
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'}}
contact1 == contact2为false了,因为它们指向了不同的内存地址了,此时修改msg1的contact属性,msg2随之而改变。
Prototype模式应用场景
原型模式通常适用于以下场景。
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
- 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
- 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
- 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。
在Spring中,如果一个类被标记为prototype
,每一次请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)都会产生一个新的bean实例。
小结
- Java自带原型模式,Object类提供了clone方法
- 要实现原型模式必须实现Cloneable接口
- 重写clone方法,如果之重写clone,而未实现Cloneable接口,调用时会出现异常
- 该模式用于对一个对象的属性已确定,需产生很多相同对象的时候
- 注意区分深克隆与浅克隆
点个赞再走吧~
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。