- 新建一个对象太复杂了,而且创建的对象大多数属性都是一致的,只有个别字段值不同
- 数据库读到数据后,在service层处理的时候需要不停的set/get,显得很费力
- 业务扩展,某个实体需要新增字段,需要在其他涉及该实体赋值的对象需要补充set/get
上面的问题都太常见,又太痛苦了,那么怎么解决这个问题呢?我们的原型设计模式闪亮登场!
**概念:**原型模式(Prototype Pattern)是指用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
顾名思义就是根据已有对象,复制一个新对象。
一、浅克隆
典型的原型模式需要我们自己创建一个Prototype接口,并定义一个clone接口,将需要复制的对象的类实现该接口,并重写clone方法,在clone方法中实现具体的细节。但是java给我们提供了一个非常好的解决方法,这里只介绍java给我们提供的clone方式。
先创建一个类,叫MingRen,它有性别、年龄、技能列表、宠物四个属性:
public class MingRen implements Cloneable {
private String sex;
private int age;
private List<String> skills;
private Pet pet;
//为节省篇幅set/get方法略
...
@Override
public String toString() {
return "MingRen{" +
"sex='" + sex + '\'' +
", age=" + age +
", skills=" + skills +
", pet=" + pet +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
其中宠物类如下:
public class Pet {
private String name;
private String type;
private List<String> skills;
...
@Override
public String toString() {
return "Pet{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
", skills=" + skills +
'}';
}
}
然后我们在测试类中先初始化一个鸣人:
public class PrototypeTest {
public static void main(String[] args) {
MingRen mingRen = new MingRen();
mingRen.setSex("male");
mingRen.setAge(15);
List<String> mingrenSkills = new ArrayList<>();
mingrenSkills.add("螺旋丸");
mingrenSkills.add("影分身");
mingRen.setSkills(mingrenSkills);
Pet hamaji = new Pet();
hamaji.setName("蛤蟆吉");
hamaji.setType("蛤蟆");
List<String> hamajiSkills = new ArrayList<>();
hamajiSkills.add("找人");
hamajiSkills.add("搞笑");
hamaji.setSkills(hamajiSkills);
mingRen.setPet(hamaji);
System.out.println("没克隆前的源对象:" + mingRen);
}
}
打印的结果如下:
没克隆前的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
可以看到,这个对象的创建已经有一定的复杂性了,既要考虑鸣人自己的技能,还得考虑他召唤出来的召唤兽的技能。而一旦鸣人释放影分身之术,将会有大量的鸣人1、2、3出现,每一个都是真的,总不能每个都这么创建一个吧。于是呼,只需要让鸣人类实现Cloneable接口,并重写clone方法,调用并返回其父类的clone方法,就可以调用clone方法复制这个对象了。
测试代码新增clone方法如下:
try{
MingRen mingRen1 = (MingRen) mingRen.clone();
mingRen1.setSex("female");
mingRen1.getSkills().add("手里剑");
System.out.println("克隆后的源对象:" + mingRen);
System.out.println("克隆对象:" + mingRen1);
System.out.println(mingRen.getSkills() == mingRen1.getSkills());
System.out.println(mingRen.getPet() == mingRen1.getPet());
}catch (CloneNotSupportedException e){
e.printStackTrace();
}
看看输出结果:
没克隆前的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆后的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身, 手里剑], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆对象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手里剑], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
true
true
先看看克隆对象,鸣人释放色诱之术影分身可以看成是女的。只修改了sex属性,并且该分身为了保护自己新增了一个手里剑技能,结果看起来很完美,所有属性一个不落的复制到了。
接下来再看看对克隆后的对象进行修改,会发现源对象也被修改了。说明这个克隆对象的方法是存在一定局限性的。只有基本类型和String类型可以真复制,其他的引用类型复制的都是其引用地址。如果需要复制对象属性而不是引用则需要用到深克隆的技术。
在将深克隆之前,先介绍一个spring为我们提供的BeanUtils里的copyProperties方法,api很简单只需要传入source对象及target对象即可。样例代码如下:
MingRen mingRen3 = new MingRen();
BeanUtils.copyProperties(mingRen, mingRen3);
但是这样复制出来的仍然是浅克隆,用来作为赋值非常方便,还可以选择忽略哪些字段不复制,更加详细的将会在spring源码解析中讲解。
二、深克隆
1、利用序列化来实现深克隆,将MingRen及Pet实现Serializable接口,并补充如下代码:
@Override
protected Object clone(){
return deepClone();
}
private Object 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);
MingRen mingRenCopy = (MingRen) ois.readObject();
ois.close();
bis.close();
oos.close();
bos.close();
return mingRenCopy;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
再次运行测试类,将会得到如下结果:
没克隆前的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆后的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆对象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手里剑], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
false
false
很显然这样的克隆确保了每一个引用对象都是新的。细心的人应该会问,为什么都是引用类型,我只给Pet实现序列化呢?查看源码就知道了,ArrayList本身已经实现了序列化,因此利用序列化及反序列化是完全可行的。
2、利用fastjson复制对象
这个也是项目中经常用到的,只需要引入fastjson的包,直接调用其api即可,代码如下:
//利用FastJson复制对象
String json = JSON.toJSONString(mingRen);
MingRen mingRen2 = JSON.parseObject(json, MingRen.class);
mingRen2.setSex("female");
mingRen2.getSkills().add("手里剑");
System.out.println("克隆后的源对象:" + mingRen);
System.out.println("克隆对象:" + mingRen2);
System.out.println(mingRen.getSkills() == mingRen2.getSkills());
System.out.println(mingRen.getPet() == mingRen2.getPet());
输出结果如下:
没克隆前的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆后的源对象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆对象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手里剑], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
false
false
可以看出,复制出来的对象也是完全ok的
三、原型模式与单例模式
可以看出,原型模式和单例模式是完全相悖的。那么如果要保持单例模式,那么还需要在单例模式里加上一些限制。
1、不实现Cloneable接口
不实现该接口,也就不用重写clone方法,自然就不会存在克隆。
2、重写clone方法
如果已经实现了,那么久重写clone方法,让其直接放回单例的实例,或者直接返回getInstance方法也是非常方便的
小结:原型模式是非常简单的一种设计模式,但是在实际项目开发中确实使用率相当高的一种模式。或许你已经在使用了,只是还没有意识到罢了!