原型模式
原型模式-当创建给定类的实例的过程很昂贵或很复杂时,就使用原型模式(Prototype Pattern)不需要知道任何创建的细节,不调用构造函数。类型:创建型
模式和结构定义
原型是用来生成实例的,但不是利用类来生成实例,而是通过实例来生成实例。
为什么我们需要用过类来生成实例呢?
联想到浏览器中,如果我们生成了一个button实例,这个button实例经过一系列操作,携带了各种信息,比如button加颜色,加背景图,加文字,加事件等等。如果我们这时候需要和这个button实例完全一样的一个实例,仅仅通过类new 一个button出来是远远不够的,因为我们还要对它进行一系列的操作,所以这个生成一个完全一样的实例的过程是非常复杂的,所以这时候我们就想到可不可以直接根据这个实例,然后生成一个一模一样的实例呢?
实际上,这就是原型模式的基本思想,根据实例原型和实例模式来生成新的实例。
Java中要实现原型模式,也就是实例的复制,我们可以直接利用clone方法,需要实现cloneable接口。
应用实例
- Prototype是一个原型的抽象类或借口,它里面有一个共有方法,叫clone
- ConcretePrototype1与ConcretePrototype2是两个具体的实例,继承或实现了Prototype。这就对应了定义中用原型实例指定创建对象的种类。
- Client是客户端类,它与Prototype是关联的关系,即在Client类的实例中,有Prototype的对象。客户端可以通过调用Prototype的clone方法来对实现了Prototype的ConcretePrototype1或ConcretePrototype2的对象进行复制来创建新对象,这样比new会有更高的执行效率。
下面使用原型模式来实现一个小猪佩奇的例子
小猪佩奇类
/**
* 小猪佩奇类
*
* @author shengyong.huang
* @date 2020-07-25
*/
public class Pig implements Cloneable {
private String name;
private Date birthday;
public Pig(String name, Date birthday) {
this.name = name;
this.birthday = birthday;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Pig pig = (Pig) super.clone();
// 深克隆
// 如果不将birthday也进行clone
// 最后得到的克隆对象中的birthday对象还是原来实例中的birthday对象
pig.birthday = (Date) pig.birthday.clone();
return pig;
}
@Override
public String toString() {
return "Pig{" +
"name='" + name + '\'' +
", birthday=" + birthday +
'}' + super.toString() + "-" + this.birthday.toString();
}
}
测试方法
/**
* 测试方法
*
* @author shengyong.huang
* @date 2020-07-25
*/
public class TestMain {
public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Date birthday = new Date(0L);
Pig pig1 = new Pig("佩奇", birthday);
Pig pig2 = (Pig) pig1.clone();
System.out.println(pig1);
System.out.println(pig2);
pig1.getBirthday().setTime(666666666666L);
System.out.println(pig1);
System.out.println(pig2);
}
}
结果展示
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}nny.design.patterns.prototype.Pig@5e2de80c-Thu Jan 01 08:00:00 CST 1970
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}nny.design.patterns.prototype.Pig@1d44bcfa-Thu Jan 01 08:00:00 CST 1970
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}nny.design.patterns.prototype.Pig@5e2de80c-Sat Feb 16 09:11:06 CST 1991
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}nny.design.patterns.prototype.Pig@1d44bcfa-Thu Jan 01 08:00:00 CST 1970
注意Pig类中的“pig.birthday = (Date) pig.birthday.clone();”
如果不将birthday也进行clone,最后得到的克隆对象中的birthday对象还是原来实例中的birthday。结果为浅拷贝。在实际编码中风险很大,而且很难排查
优点和不足
优点
- 原型模式性能比直接new一个对象性能高
- 简化创建过程
缺点
- 必须配备克隆方法
- 对克隆复杂对象或对克隆出的对象进行复杂改造时,容易引入风险
- 深拷贝、浅拷贝要运用得当
使用场景
- 类初始化消耗较多资源
- new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)
- 构造函数比较复杂
- 循环体中生产大量对象时
补充
浅克隆和深克隆
原型模式的克隆分为浅克隆和深克隆,在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括
int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。
1.浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制
Java 中的 Object 类提供了的 clone() 方法为浅克隆,如果原型对象中存在引用类型,那么克隆对象将复制该引用对象的地址。也就说通过“==”判断克隆与被克隆的对象的话,值类型的结果会是flase,引用类型会是true。
2.深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。下面我们使用深克隆技术来实现工作周报和附件对象的复制,由于要将附件对象和工作周报对象都写入流中,因此两个类均需要实现Serializable接口。
**java引用类型默认都是浅拷贝,很容易出现内部的属性对象引用的还是原来的实例的属性对象。
**
参考:
https://www.jianshu.com/p/1939fbe5dfdb
https://blog.csdn.net/yucaixiang/article/details/90379525