原型模式是一种设计模式,这有点像废话,其实我说的是它不局限或者说拘泥于某种特定的实现方法,只要你按这种套路来,它就是原型模式。
原型模式指的是照着原有对象,克隆出一个新的但是一模一样的对象。怎么理解这句话呢,一般来说,new出一个对象之后,取名obj1,将它赋给obj2:obj2 = obj1,实际上只是把obj1指向的内存空间的引用地址赋给了obj2,修改obj1的时候,实际上obj2也会改变,这是非常不安全的,这也不能叫克隆,克隆是指新对象和被克隆的对象拥有相同的参数和信息,但是修改其中任意一个的时候互不影响。
原型模式适用于创建一个对象代价很大很复杂的时候,用克隆的方式实现对原有对象的复制。
现在大部分都是实现Cloneable接口,来指明这个java类(要被克隆的类)可以被复制,然后使用Object提供的clone()方法来实现对象的克隆,实际上在使用这个固定的套路之前可以先用一段简单的代码来自己实现克隆,加深对这个过程的理解。
1.通用代码实现克隆
//被克隆的原型
public class ProtoType{
private String attr;
public String getAttr() {
return attr;
}
public void setAttr(String attr) {
this.attr = attr;
}
public ProtoType clone(){//内写一个clone方法,用来返回一个新的自身对象,同时对数据进行复制
ProtoType pt = new ProtoType();
pt.setAttr(this.attr);//同时把数据进行复制
return pt;
}
}
//调用
public class Test {
public static void main(String[] args){
ProtoType pt = new ProtoType();//创建一个原型类的对象
pt.setAttr("hello Prototype Pattern");//对原型类添加数据
ProtoType pt2 = pt.clone();//对原型类对象进行克隆
System.out.println(pt2 == pt);//对引用类型的对象进行比较,结果为false,证明实现了克隆
}
}
输出结果为:false
这就是简单用代码对原型模式的克隆实现了一下,理解这个原型模式到底是什么意思,但是我们依然要在clone()方法里进行new的操作,增加了系统开销,实际上会用像文章开头所说的,使用基类Object的clone()方法来实现对原型对象的复制。
2. Object类的clone()方法实现克隆
原型类(被克隆的类)需要实现Cloneable接口,它只有一个作用,就是在JVM运行时告知它可以安全的使用clone方法,打开Cloneable接口源码可以看到,其内部没有任何方法, 仅是作为一个标识作用。如果没有实现Cloneable接口,将会报出CloneNotSupportedException异常。
因为在调用Object类的clone()方法时会对原型类进行复制,同时也会对原型类对其他对象的引用也进行复制,这时候就会存在一个浅度克隆和深度克隆的问题:
- 浅度克隆:只负责克隆按值传递的数据(比如基本数据类型、String类型),而不复制它所引用的对象,换言之,所有的对其他对象的引用都仍然指向原来的对象。
- 深度克隆:除了浅度克隆要克隆的值外,还负责克隆引用类型的数据。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深度克隆把要复制的对象所引用的对象都复制了一遍,而这种对被引用到的对象的复制叫做间接复制。
深度克隆要深入到多少层,是一个不易确定的问题。在决定以深度克隆的方式复制一个对象的时候,必须决定对间接复制的对象时采取浅度克隆还是继续采用深度克隆。因此,在采取深度克隆时,需要决定多深才算深。此外,在深度克隆的过程中,很可能会出现循环引用的问题,必须小心处理。
2.1 浅度克隆
//首先实现Cloneable接口标记此类可以被克隆,不然会抛出异常
public class ProtoType implements Cloneable{
//传入一些数据,用来验证克隆后数据是否被复制
private int age;
private String name;
private List<String> list;
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;
}
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
//java提供的clone方法
public ProtoType clone(){
try {
return (ProtoType)super.clone();//浅度克隆
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
//调用
public class Test {
public static void main(String[] args){
ProtoType pt = new ProtoType();//创建一个原型类对象
pt.setAge(15);//对原型类传入参数
pt.setName("Peter");
List<String> list = new ArrayList();
list.add("test1");
pt.setList(list);
ProtoType pt2 = pt.clone();//使用原型类重写的clone()方法进行克隆,复制给pt2
System.out.println("1:"+pt2.getAge());//输出克隆后的数据
System.out.println("2:"+pt2.getName());
System.out.println("3:"+pt2.getList().size());
//重新更改原型类pt的数据,验证克隆后的pt2数据是否会一同被改变。
//(浅度复制的话对引用对象复制的是内存地址,pt更改数据后pt2的引用类型数据也会改变,非常不安全,String、Integer等包装类型除外)
pt.setAge(16);
pt.setName("tom");
list.add("test2");
pt.setList(list);
System.out.println("1:"+pt2.getAge());//重新输出pt2的数据
System.out.println("2:"+pt2.getName());
System.out.println("3:"+pt2.getList().size());//这里发生了改变
}
}
输出结果:
1:15
2:Peter
3:1
1:15
2:Peter
3:2
事实证明,除了基本数据类型和包装类型外,其他的引用类型只是复制了内存的指向地址,当原型类的数据发生改变时,克隆的类也会被更改(基本数据类型和包装类型除外),接下来就要说一下深度克隆。
2.2 深度克隆
public class ProtoType implements Cloneable{
private int age;
private String name;
private List<String> list;
//这里隐去set/get方法代码,和浅度克隆一样
public ProtoType clone(){
try {
ProtoType protoType = (ProtoType)super.clone();
List<String> newList = new ArrayList();
for(String str : this.list){//区别在这里,这里对引用类型的数据进行了重新赋值
newList.add(str);
}
protoType.setList(newList);//把新地址的数据set进去
return protoType;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
此时再调用后输出的结果:
1:15
2:Peter
3:1
1:15
2:Peter
3:1
此时证明,无论原型类的引用类型数据再怎么更改,克隆后的数据也不会发生改变,因为此时引用类型数据指向的空间地址是新数据的空间地址。
总结:
- 当我们创建一个新对象(new)时开销很大,或者很繁琐时,可以使用原型模式,因为clone()方法是在内存中对字节码进行复制,大大降低了系统的开销。
- 当我们在获得基类的基本结构之外还希望获得基类的数据时。
- 希望对目标对象的修改不影响既有的原型对象(深度克隆的时候可以完全互不影响)时。
缺点:
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
扩展:我们还可以实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,主要用来解决多层克隆问题
主要是利用对对象序列化之后进行反序列化,实现对对象的克隆。
首先需要被克隆的类实现Serializable接口。
代码:
public class ProtoType implements Serializable{
private int age;
private String name;
private List<String> list;
//省略set/get方法......
@Override
public ProtoType clone(){
ByteArrayOutputStream baos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bais = null;
ObjectInputStream ois = null;
try {
//序列化将对象写入流中
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(this);
//反序列化将对象从流中读取出来
bais = new ByteArrayInputStream(baos.toByteArray());
ois = new ObjectInputStream(bais);
ProtoType pt = (ProtoType)ois.readObject();
return pt;
} catch (IOException e) {
e.printStackTrace();
return null;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}finally{
try {
baos.close();
oos.close();
bais.close();
ois.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//调用方法同上
再次调用输出结果:
1:15
2:Peter
3:1
1:15
2:Peter
3:1
事实证明利用序列化深度克隆成功,成功产生新的引用对象,不需要实现Cloneable接口,重写Clone()方法。
以上就是原型模式实现三种克隆的具体实现,在深入了解这些设计模式的同时也是对java认知的不断加深。