介绍
原型模式(prototype Pattern)是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式。
原型模式的核心在于拷贝原型对象,以系统中已存在的一个对象为原型,无需经历耗时的对象初始化过程,不调用 new 构造函数。
我们看下UML图:
通用写法
看完上面的介绍,我们来写一个原型模式的通用写法。先创建原型IPrototype接口:
public interface IPrototype<T> {
T clone();
}
创建具体需要克隆的对象ConcretePrototype
@Data
public class ConcretePrototype implements IPrototype {
private int age;
private String name;
@Override
public ConcretePrototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(this.age);
concretePrototype.setName(this.name);
return concretePrototype;
}
}
测试代码:
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("Jack");
System.out.println(prototype);
//拷贝原型对象
ConcretePrototype cloneType = prototype.clone();
System.out.println(cloneType);
}
运行结果:
一个简单的原型模式就写完了,在这个简单的场景之下,看上去操作好像变的复杂了,但如果有几百个属性需要复制,那我们就可以一劳永逸。但是,上面的复制过程是我们自己完成的,在实际编码中,我们一般不会浪费这样的体力劳动,JKD已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。
浅克隆
新建一个ShallowConcretePrototype类实现Cloneable接口
@Data
public class ShallowConcretePrototype implements Cloneable {
private int age;
private String name;
private List<String> hobbies;
@Override
public ShallowConcretePrototype clone() {
try {
return (ShallowConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
写一段测试代码
public static void main(String[] args) {
//创建原型对象
ShallowConcretePrototype prototype = new ShallowConcretePrototype();
prototype.setAge(18);
prototype.setName("Jack");
List<String> hobbies = new ArrayList<String>();
hobbies.add("看书");
hobbies.add("旅游");
prototype.setHobbies(hobbies);
//拷贝原型对象
ShallowConcretePrototype cloneType = prototype.clone();
System.out.println("原型对象:" + prototype);
System.out.println("克隆对象:" + cloneType);
System.out.println("——————————————————————分割线————————————————————————");
cloneType.getHobbies().add("技术控");
System.out.println("原型对象:" + prototype);
System.out.println("克隆对象:" + cloneType);
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
执行结果:
我们来解读一下,首先创建一个原型对象,然后使用clone方法创建一个克隆对象,打印一下,两者是一样的,没问题。然后我给克隆对象的爱好添加了一个技术控,然后在打印一下,发现原型对象的爱好里也多了一个技术控,这个是不符合我们预期的,我们希望的是克隆对象和原型对象是两个独立的对象,我添加我的,你添加你的,不能因为我添加了影响到你。
上面的这个现象就叫浅克隆,通过我们分析下来,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用的地址。最后我们通过prototype.getHobbies() == cloneType.getHobbies()测试为true,证明确实它们的地址是一样的。
深克隆
为了解决上面的问题,下面就引出我们的深克隆,创建一个DeepConcretePrototype类,增加一个deepClone的方法。
@Data
public class DeepConcretePrototype implements Cloneable, Serializable {
private int age;
private String name;
private List<String> hobbies;
@Override
public DeepConcretePrototype clone() {
try {
return (DeepConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public DeepConcretePrototype 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 (DeepConcretePrototype) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
测试代码和上面一样,只不过在克隆的时候使用的是deepClone方法,而不是clone方法。
public static void main(String[] args) {
//创建原型对象
DeepConcretePrototype prototype = new DeepConcretePrototype();
prototype.setAge(18);
prototype.setName("Jack");
List<String> hobbies = new ArrayList<String>();
hobbies.add("看书");
hobbies.add("旅游");
prototype.setHobbies(hobbies);
//拷贝原型对象
DeepConcretePrototype cloneType = prototype.deepClone();
System.out.println("原型对象:" + prototype);
System.out.println("克隆对象:" + cloneType);
System.out.println("——————————————————————分割线————————————————————————");
cloneType.getHobbies().add("技术控");
System.out.println("原型对象:" + prototype);
System.out.println("克隆对象:" + cloneType);
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
执行一下,这时候就达到了我们预期想要的效果了。
在这里deepClone方法中用的是输入输出流的方法,还可以用Jackson的序列化和反序列话来实现深克隆;也可以不增加deepClone方法,直接在clone方法里调用super.clone(),然后手动的给hobbies赋值。
原型保证单例
如果有一个类实现了Cloneable接口,又要保证它单例,那么只需要在clone方法中返回单例对象即可。
@Data
public class SingletonConcretePrototype implements Cloneable {
private int age;
private String name;
private List<String> hobbies;
private static SingletonConcretePrototype instance = new SingletonConcretePrototype();
private SingletonConcretePrototype() {}
public static SingletonConcretePrototype getInstance() {
return instance;
}
@Override
public SingletonConcretePrototype clone() {
return instance;
}
}
或者就是不实现Cloneable接口,因为本身单例和原型是矛盾的,单例要求的是全局只有一个对象,而原型则是创造出很多个对象,那到底是一个还是多个?这一点在Sping中的scope就是要么填singleton要么填prototype。
更多关于单例的知识请点击:《深入解析单例模式的写法以及破坏单例方式》
原型模式应用
在Java代码中查找实现了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);
}
}
这里将List中的元素循环遍历了一遍,这个就是深克隆,我们来验证一下
public static void main(String[] args) {
ArrayList<String> stringList = new ArrayList<>();
stringList.add("a");
stringList.add("b");
stringList.add("c");
ArrayList<String> cloneList = (ArrayList<String>) stringList.clone();
System.out.println("原型对象:" + stringList);
System.out.println("克隆对象:" + cloneList);
System.out.println("——————————————————————分割线————————————————————————");
cloneList.add("d");
System.out.println("原型对象:" + stringList);
System.out.println("克隆对象:" + cloneList);
}
添加完d后,没有影响到原型对象,证明是深克隆。
优缺点
优点:
- 性能优良,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点:
- 需要为每一个类配置一个克隆方法。
- 克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。
深克隆、浅克隆看具体应用场景选择。 本文源码在:github.com/xuhaoj/patt…