“这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战”
一、原型模式的定义
原型模式(Prototype Pattern)是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式。
原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升许多。当对象的构建过程比较耗时时,可以利用当前系统中已存在的对象作为原型,对其进行克隆(一般是基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大减少。
原型模式主要包含三个角色:
- 客户(client):客户类提出创建对象的请求。
- 抽象原型(prototype):规定拷贝接口。
- 具体原型(concrete prototype):被拷贝的对象。
注意:对不通过new关键字,而是通过对象拷贝来实现创建对象的模式就称作原型模式。
二、原型模式的应用场景
你一定遇到过大篇幅getter、setter赋值的场景。(大多的是体力劳动~~~)
1. 原型模式主要适用于以下场景
- 类初始化消耗资源较多。
- new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)
- 构造函数比较复杂。
- 循环体中生产大量对象时。
在Spring中,原型模式应用得非常广泛。例如scope="prototype",在我们经常用的JSON.parseObject()也是一种原型模式。
2. 原型模式的通用写法
一个标准的原型模式代码,应该是这样设计的。先创建原型IPrototype接口:
public interface IPrototype<T> {
T clone();
}
创建具体需要克隆的对象ConcretePrototype:
@Data
public class ConcretePrototype implements Cloneable {
private String name;
private int age;
@Override
public ConcretePrototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setName(this.name);
concretePrototype.setAge(this.age);
return concretePrototype;
}
}
测试代码:
public class PrototypeTest {
@Test
public void test() {
//创建原型对象
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setName("Mark");
concretePrototype.setAge(18);
System.out.println(concretePrototype);
//拷贝原型对象
ConcretePrototype cloneType = concretePrototype.clone();
System.out.println(cloneType);
}
}
运行结果:
这时候,有小伙伴就问了,原型模式就这么简单么?对,就是这么简单。在这个简单的场景之下,看上去操作好像变复杂了。但如果有几百个属性需要复制,那我们就可以一劳永逸。但是,上面的复制过程是我们自己完成的,在实际编码中,我们一般不会浪费这样的体力劳动,JDK已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。来改造一下代码,修改ConcretePrototype类:
@Data
public class ConcretePrototype implements Cloneable {
private String name;
private int age;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
重新运行,也会得到同样的结果。有了JDK的支持再多的属性复制我们也能轻而易举地搞定了。下面我们再来一个测试,给ConcretePrototype类增加一个个人爱好的属性hobbies:
@Data
public class ConcretePrototype implements Cloneable {
private String name;
private int age;
private List<String> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
修改客户端测试代码:
public class PrototypeTest {
@Test
public void test() {
//创建原型对象
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setName("Mark");
concretePrototype.setAge(18);
List<String> hobbies = new ArrayList<>();
hobbies.add("basketball");
hobbies.add("coding");
concretePrototype.setHobbies(hobbies);
//拷贝原型对象
ConcretePrototype cloneType = concretePrototype.clone();
cloneType.getHobbies().add("swing");
System.out.println("原型对象:" + concretePrototype);
System.out.println("克隆对象:" + cloneType);
}
}
运行结果:
我们给复制后的克隆对象新增一项爱好后,发现原型对象也发生了变化,这显然不符合我们的预期。因为我们希望克隆出来的对象应该和原型对象是两个独立的对象,不应该再有联系了。从测试结果分析来看,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用的地址。这样的话,如果我们修改任意一个对象中的属性值,prototype和cloneType的bobbies值都会改变。这就是我们常说的浅克隆。 只是完整复制了值类型数据,没有复制引用对象。换言之,所有的引用对象仍然指向原来的对象,显然不是我们想要的结果。那如何解决这个问题呢?下面我们来看深度克隆继续改造。
3. 使用序列化实现深度克隆
在上边的代码基础上我们继续改造,来看代码,增加一个deepClone()方法:
@Data
public class ConcretePrototype implements Cloneable, Serializable {
private String name;
private int age;
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;
}
}
}
来看客户端调用代码:
@Data
public class ConcretePrototype implements Cloneable, Serializable {
private String name;
private int age;
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;
}
}
}
运行结果如下:
三、原型模式在源码中的应用
先来看JDK中的Cloneable接口:
public interface Cloneable {
}
接口定义还是很简单的,我们找到源码其实只需要找到看哪些接口实现了Cloneable接口即可。来看ArrayList类的实现。
我们发现方法中只是将List中的元素循环遍历了一遍。这个时候我们再思考一下,是不是这种形式就是深克隆呢?其实用代码验证一下就知道了,继续修改ConcretePrototype类,增加一个deepCloneHobbies()方法:
public ConcretePrototype deepCloneHobbies() {
try {
ConcretePrototype result = (ConcretePrototype) super.clone();
result.setHobbies((List) ((ArrayList) result.getHobbies()).clone());
return result;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
修改客户端代码:
public class PrototypeTest {
@Test
public void test() {
//创建原型对象
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setName("Mark");
concretePrototype.setAge(18);
List<String> hobbies = new ArrayList<>();
hobbies.add("basketball");
hobbies.add("coding");
concretePrototype.setHobbies(hobbies);
//拷贝原型对象
ConcretePrototype cloneType = concretePrototype.deepCloneHobbies();
cloneType.getHobbies().add("swing");
System.out.println("原型对象:" + concretePrototype);
System.out.println("克隆对象:" + cloneType);
System.out.println(concretePrototype == cloneType);
}
}
运行结果如下:
运行也能得到期望的结果。但是这样的代码,其实都是硬编码,如果在对象中声明了各种集合类型,那每种情况都需要单独处理。因此,深克隆的写法,一般会直接用序列化来操作。
四、原型模式的优缺点
优点:
- 性能优良,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了很多。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一个状态),可辅助实现撤销操作。
缺点:
- 需要为每一个类配置一个克隆方法。
- 克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深拷贝、浅拷贝需要运用得当。
五、友情链接
欢迎大家关注微信公众号(MarkZoe)互相学习、互相交流。