1.定义
- 原型模式(Prototype Pattern)是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
- 调用者不需要知道任何创建细节,不调用构造函数。
- 属于创建型模式
原型模式的核心在于拷贝原型对象 。以系统中已存在的一个对象为原型,直接基于内存二进制流进
行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数) 性能提升许多。当对象的构建过程比较耗时时,可以利用当前系统中已存在的对象作为原型,对其进行克隆(一般是基于二进制流的复制)躲避初始化过程,使得新对象的创建时间大大减少。
注:对不通过new关键字,而是通过对象拷贝来实现创建对象的模式就称作原型模式
2.原型模式适用场景
- 类初始化消耗资源较多
- new产生的一个对象重要非堂整琐的过程 (数据准备、 访问权限等
- 构造函数比较复杂。
- 循环体中生产大量对象时
在Spring中,原型模式应用得非常广泛。 例如 scope="prototype"在我们经常用的JSON.parseObject()也是一种原型模式。
3.原型的通用写法
- IPrototype接口
public interface IPrototype<T> {
T clone();
}
- 创建具体需要克隆的对象ConcretePrototype继承原型接口
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(age);
concretePrototype.setName(name);
return concretePrototype;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
- 测试代码
public class Client {
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(12);
prototype.setName("小郑");
// 拷贝原型对象
ConcretePrototype clone = prototype.clone();
System.out.println(prototype);
System.out.println(clone);
}
}
- 运行结果
- 类图
我们可以看到,原型模式 主要包含三个角色
- 客户(Client):客户类提出创建对象的请求
- 抽象原型(Prototype):规定拷贝接口。
- 具体原型 (Concrete Prototype): 被拷贝的对象
4.分析原型浅克隆所带来的的问题
看完原型的通用写法,有小伙伴就问了,原型模式就这么简单吗?对,就是这么简单。在这个简单的场景之下,看上去操作好像变复杂了。但如果有几百个属性需要复制,那我们就可以一劳永逸。但是,上面的复制过程是我们自己完成的,在实际编码中,我们一般不会浪费这样的体力劳动,JDK 已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。-
- 修改ConcretePrototype类实现Cloneable接口:
@Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
重新运行,也会得到同样的结果。有了JDK的支持再多的属性复制我们也能轻而易举地搞定了。下面我们再来做一个测试,给ConcretePrototype增加一个个人爱好的属性hobbies
- ConcretePrototype增加属性
@Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
private List<String> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
- 修改客户端测试代码
public class Client {
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(12);
prototype.setName("小郑");
List<String> hobbies = new ArrayList<>();
hobbies.add("书法");
hobbies.add("弹琴");
prototype.setHobbies(hobbies);
// 拷贝原型对象
ConcretePrototype clone = prototype.clone();
clone.getHobbies().add("美术");
System.out.println(prototype);
System.out.println(clone);
}
}
- 结果
我们给,复制后的克隆对象新增一项爱好,发现原型对象也发生了变化,这显然不符合我们的预期,因为我们希望克隆出来的对象应该和原型对象是两个独立的对象,不应该再有联系了。从测试结果分析来看,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用的地址。这样的话,如果我们修改任意一个对象中的属性值, prototype 和colneType的hobbies值都会改变。这就是我们常说的 浅克隆 。 只是完整复制了值类型数据,没有赋值引用对象 。换言之,所有的引用对象仍然指向原来的对象,显然不是我们想要的结果。那如何解决这个问题呢?下面我们来看深度克隆继续改造
5.使用序列化实现深克隆
在上面的基础上我们继续改造,增加一个deepClone()方法:
- ConcretePrototype类
@Data
public class ConcretePrototype implements Cloneable, Serializable {
private int age;
private String name;
private List<String> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public ConcretePrototype deepClone() {
try{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (ConcretePrototype) ois.readObject();
}catch(Exception e){
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
- 测试
public class Client {
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(12);
prototype.setName("小郑");
List<String> hobbies = new ArrayList<>();
hobbies.add("书法");
hobbies.add("弹琴");
prototype.setHobbies(hobbies);
// 拷贝原型对象
ConcretePrototype clone = prototype.deepClone();
clone.getHobbies().add("美术");
System.out.println(prototype);
System.out.println(clone);
}
}
- 结果
6.克隆破坏单例模式
如果我们克隆的目标的对象是单例对象那意味着,深克隆就会破坏单例。实际上防止克隆破坏单例解决思路非常简单,禁止深克隆便可。要么你我们的单例类不实现Cloneable 接口:要么我们重写clone)方法,在cone方法中返回单例对象即可,具体代码如下
@Override
public ConcretePrototype clone() {
return INSTANCE;
}
7.总结
原型模式的优点
- 性能优良,Java 自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作
原型模式的缺点
- 需要为每一个类配置一个克隆方法。
- 克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违反了开闭原则
- 在实现深克隆时需要编写较为复杂的代码,而目当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深拷贝、浅拷贝需要运用得当