原型模式
原型模式是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,数据创建型模式。
原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时和对象初始化过程(不用调用构造函数),性能提升很多。当对象的构建过程比较耗时的时候,可以利用当前系统中已存在的对象作为原型,对其进行克隆(一般是基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大减少。下面我们来看看原型模式类结构图:
从UML图中,我们可以看到,原型模式主要包括三个角色:
客户(Client):客户类提出创建对象的请求。
抽象原型(Prototype):规定拷贝接口。
具体原型(Concrete Prototype):被拷贝的对象。
原型模式的应用场景
你一定遇到过大篇幅,get,set复制的场景。例如这样的代码:
User user = new User();
user.setName(userVo.getName());
user.setAge(userVo.getAge());
...
复制代码
代码非常工整,命名非常规范,但是这样的代码优雅么?这就是纯体力活,如果一个实体类字段有二十个,那你就需要get,set二十次,那么原型模式,能帮助我们解决这样的问题。
原型模式主要适用于以下场景:
- 类初始化消耗资源较多
- new产生的一个对象需要非常繁琐的过程(数据猪呢比,访问权限等)
- 构造函数比较复杂
- 循环体中产生大量对象时
在Spring中,原型模式应用得也非常广泛。例如 scope=”prototype”,在我们经常用的JSON.parseObject()也是一种原型模式
原型模式的通用写法
一个标准的原型模式代码,应该是这样设计的。先创建原型IPrototype接口:
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(this.age);
concretePrototype.setName(this.name);
return concretePrototype;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
//创建原型对象
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(18);
concretePrototype.setName("lihaha");
System.out.println(concretePrototype);
//拷贝对象
ConcretePrototype cloneType = concretePrototype.clone();
System.out.println(cloneType);
}
}
复制代码
结果:
像我们项目中,跟前端打交道的是VO,需要把DTO的数据转换成VO,但是有一部分数据不能给用户看到。这时候,如果正常流程应该是:
UserVo userVo = new UserVo();
userVo.setName(userDto.getName());
userVo.setAge(userDto.getAge());
//这里UserDto还有很多属性,比如密码等信息是不能给前端返回的,所以就不会给VO赋值
复制代码
这里insert,update,等都需要调用,调用非常的多,也可能其他地方也会调用,每个都要写一遍真的累死了
原型模式改造:
UserDto:
public class UserDto implements IPrototype{
private String name;
private int age;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "UserDto{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + password + '\'' +
'}';
}
@Override
public UserVo clone() {
UserVo userVo = new UserVo();
userVo.setAge(this.age);
userVo.setName(this.name);
return userVo;
}
public static void main(String[] args) {
UserDto userDto = new UserDto();
userDto.setAge(1);
userDto.setName("lihaha");
userDto.setPassword("123");
System.out.println(userDto.toString());
UserVo userVo = userDto.clone();
System.out.println(userVo.toString());
}
}
复制代码
UserVo:
public class UserVo {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "UserVo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
复制代码
结果:
这时候,就有人有怀疑了,原型模式就这么简单么?对,就这么简单。在简单的场景下,看上去好像操作变复杂了,但是如果属性比较多,那我们就可以一劳永逸了。但是,上面的复制过程是我们自己完成的,在实际编码中,我们一般不会浪费这样的体力劳动,JDK已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。来改造一下代码:
public class ConcretePrototype implements Cloneable{
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() {
try{
return (ConcretePrototype) super.clone();
}catch (CloneNotSupportedException e){
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
//创建原型对象
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(18);
concretePrototype.setName("lihaha");
System.out.println(concretePrototype);
//拷贝对象
ConcretePrototype cloneType = concretePrototype.clone();
System.out.println(cloneType);
}
}
复制代码
结果:
我们来改造一下代码:
public class ConcretePrototype implements Cloneable{
private int age;
private String name;
private List<String> hobbies;
public List<String> getHobbies() {
return hobbies;
}
public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}
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() {
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 static void main(String[] args) {
//创建原型对象
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(18);
concretePrototype.setName("lihaha");
List<String> hobbies = new ArrayList<>();
hobbies.add("书法");
hobbies.add("美术");
concretePrototype.setHobbies(hobbies);
System.out.println(concretePrototype);
//拷贝对象
ConcretePrototype cloneType = concretePrototype.clone();
cloneType.getHobbies().add("技术");
System.out.println("原型:"+concretePrototype);
System.out.println("克隆:"+cloneType);
}
}
复制代码
结果:
我们给复制后的克隆对象新增一项爱好,发现原型对象也发生了变化,这显然是不符合我们的预期。因为我们希望克隆出来的对象应该和原型对象是两个独立的对象,不应该再有联系了。从测试结果分析来看,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用地址。这样的话,如果我们修改任意一个对象中的属性值,prototype和cloneType的hobbies值都会改变。这就是我们说的浅克隆。只有完整复制了值类型数据,没有赋值引用对象。换言之,所有的引用对象仍然指向原来的对象,显然不是我们想要的结果。那如何解决这个问题呢?
使用序列化实现深克隆
public class ConcretePrototype implements Cloneable,Serializable{
private int age;
private String name;
private List<String> hobbies;
public List<String> getHobbies() {
return hobbies;
}
public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}
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() {
try{
return (ConcretePrototype) super.clone();
}catch (CloneNotSupportedException e){
e.printStackTrace();
return null;
}
}
public ConcretePrototype deepClone() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
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 static void main(String[] args) {
//创建原型对象
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(18);
concretePrototype.setName("lihaha");
List<String> hobbies = new ArrayList<>();
hobbies.add("书法");
hobbies.add("美术");
concretePrototype.setHobbies(hobbies);
System.out.println(concretePrototype);
//拷贝对象
ConcretePrototype cloneType = concretePrototype.deepClone();
cloneType.getHobbies().add("技术");
System.out.println("原型:"+concretePrototype);
System.out.println("克隆:"+cloneType);
}
}
复制代码
结果:
克隆破坏单例模式
如果我们的克隆的目标的对象是单例对象,那意味着,深克隆就会破坏单例。实际上防止克隆破坏单利解决思路很简单,禁止深度克隆就可以。要么单例类不实现Cloneable接口,要么我们重写clone()方法,在clone方法中返回单例对象即可,具体代码如下
protected object clone() throws CloneNotSupportedException{
return INSTANCE;
}
复制代码
原型模式在JDK源码中的应用
先来JDK中的Cloneable接口:
package java.lang;
/**
* A class implements the <code>Cloneable</code> interface to
* indicate to the {@link java.lang.Object#clone()} method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
* <p>
* Invoking Object's clone method on an instance that does not implement the
* <code>Cloneable</code> interface results in the exception
* <code>CloneNotSupportedException</code> being thrown.
* <p>
* By convention, classes that implement this interface should override
* <tt>Object.clone</tt> (which is protected) with a public method.
* See {@link java.lang.Object#clone()} for details on overriding this
* method.
* <p>
* Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
* Therefore, it is not possible to clone an object merely by virtue of the
* fact that it implements this interface. Even if the clone method is invoked
* reflectively, there is no guarantee that it will succeed.
*
* @author unascribed
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since JDK1.0
*/
public interface Cloneable {
}
复制代码
接口定义还是很简单的,我们找源码其实只需要找到看哪些接口实现了Cloneable即可,来看ArrayList类的实现
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
复制代码
原型模式的优缺点:
优点:
-
性能优良,Java自带的是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
-
可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点:
-
需要为每一个类型配置一个克隆方法
-
克隆方法位于类的内部,当对已有的类进行改造的时候需要修改代码,违反了开闭原则。
-
在实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深拷贝,浅拷贝需要运用得当。