有人这么理解原型模式:在《西游记》所有版本里,孙悟空都是其中的一号雄性主角,关于他(或它)拔毛变小猴的故事几乎人人皆知,孙悟空可以用猴毛根据自己的形象,复制(又称“克隆”或“拷贝”)出很多跟自己长得一模一样的“身外身”来。在设计模式中也存在一个类似的模式,可以通过一个原型对象克隆出多个一模一样的对象,该模式称之为原型模式
1、原型模式定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象;
2、原型模式使用
学过Java语言的人都知道,所有的Java类都继承自java.lang.Object。事实上,Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。
需要注意的是能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。如下代码所示:
package 原型模式; /*** * 原型对象类 * * @author hongzm * */ public class ConcreteUser implements Cloneable { // 成员属性 private String name; private String pwg; private int age; //省略get/set方法
@Override public ConcreteUser clone() { ConcreteUser user = null; try { user = (ConcreteUser) super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Not support cloneable"); } return user; } @Override public String toString() { return "user [ name = " + name + " ,age = " + age + " ]"; } }
声明一个ConcreteUser对象,实现了Cloneable接口,成员变量:name,pwg,age,重写的clone里面,通过super.clone()调用Object的clone方法,返回克隆的对象。
下面看下简单的使用并测试:
package 原型模式; public class DemoTest { public static void main(String[] args) { // 创建原型对象 ConcreteUser user = new ConcreteUser(); user.setName("zm"); user.setPwg("111"); System.out.println("原对象" + user); System.out.println("============"); for(int i = 0; i < 5; i++) { // 调用克隆方法 ConcreteUser copyUser = user.clone(); copyUser.setAge(i); System.out.println("对象克隆" + i + ":" + copyUser); } } }
结果:
再看下每个对象在内存中地址:(注释掉上面toString)
可以看到克隆的地址都不一样,也就是生成新的对象。
总结:
为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:
(1) 在派生类中覆盖基类的clone()方法,并声明为public;
(2) 在派生类的clone()方法中,调用super.clone();
(3)派生类需实现Cloneable接口。
此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。
3、原型模式优点
1)、性能优良
原型模式是在内存二进制流的拷贝,要比直接new一个对象的性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好的体现其优点。
2)、逃避构造函数的约束
这既是优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,优点就是减少了约束,缺点也是减少了约束
原型对象里面增加无参构造函数;
4、原型模式使用场景
1)资源优化
类初始化需要消耗非常多的资源,这个资源包括数据、硬件资源等。
2)性能和安全要求的场景
通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
3)一个对象多个修改者的场景
一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值的时候,可以考虑使用原型模式拷贝多个对象供调用者使用。
5、原型模式的注意事项
1)构造函数不会被执行
一个原型类实现了Cloneable方法并重写了clone方法的类A,原型类还有一个无参构造或有参构造B;构造函数打印被执行的信息。
package 原型模式; /*** * 原型对象类 * * @author hongzm * */ public class ConcreteUser implements Cloneable { // 成员属性 private String name; private String pwg; private int age;
//省略get/set方法 @Override public ConcreteUser clone() { ConcreteUser user = null; try { user = (ConcreteUser) super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Not support cloneable"); } return user; } //增加构造方法 public ConcreteUser() { System.out.println("构造函数被执行了..."); } }
测试:通过new关键字产生了一个对象user,再然后通过user.clone()方式产生了一个新的对象copyUser,那么在对象拷贝的时候,构造函数是不会被执行的。
@Test //测试克隆时候构造函数会不会被执行:不会 public void constructorTest() { ConcreteUser user = new ConcreteUser(); ConcreteUser copyUser = user.clone(); }
结果:只被执行了一次。说明克隆的时候,没有执行构造函数而是直接内存拷贝
2)深拷贝和浅拷贝
浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制;
浅拷贝:
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制;
深拷贝:
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
先来看下浅拷贝的例子:
原型对象:增加成员变量List数组(List传递的是引用而不是值类型),修改set方法,方便测试
package 原型模式; import java.util.ArrayList; import java.util.List; /*** * 原型对象类 * * @author hongzm * */ public class ConcreteUser implements Cloneable { // 成员属性 private String name; private String pwg; private int age; // 引用类型 private List<String> list = new ArrayList<>(); public List<String> getList() { return list; } // 修改set方法 public void setList(String value) { this.list.add(value); } //省略成员变量的get/set方法 @Override public ConcreteUser clone() { ConcreteUser user = null; try { user = (ConcreteUser) super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Not support cloneable"); } return user; } public ConcreteUser() { System.out.println("构造函数被执行了..."); } }
测试:
@Test // 测试深浅拷贝 public void copyTest() { ConcreteUser user = new ConcreteUser(); user.setList("11111"); ConcreteUser copyUser = user.clone(); copyUser.setList("22222"); System.out.println(user.getList()); }
结果
嗯哼?为什么user对象会把克隆的对象copyUser设的值也弄进来呢?
是因为Java做了一个偷懒的拷贝动作,Object类提供的方法clone只是拷贝本对象,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝。
注意:引用原型模式的时候,引用的成员遍历必须满足两个条件才不会被拷贝,
一是类的成员变量,而不是方法内变量;
二是必须是一个可变的引用对象,而不是一个原始类型或不可变对象(不可变对象如String对象)。
那怎么实现深拷贝?
在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用
的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。
需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
原型实现序列化,深克隆就需要将对象写入流中,再从流中取出
package 原型模式; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /*** * 原型对象类 * * @author hongzm * */ @SuppressWarnings("serial") public class ConcreteUser implements Serializable { // 成员属性 private String name; private String pwg; private int age; // 引用类型 private ArrayList<String> list = new ArrayList<>(); public List<String> getList() { return list; } // 修改set方法 public void setList(String value) { this.list.add(value); } //省略get/set方法 // 使用序列化实现深克隆 public ConcreteUser deepClone() throws IOException, ClassNotFoundException { // 将对象写入流中 ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(this); // 将对象从流中取出 ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (ConcreteUser) ois.readObject(); } public ConcreteUser() { System.out.println("构造函数被执行了..."); }
测试
@Test // 测试深拷贝 public void deepCopyTest() { ConcreteUser user = new ConcreteUser(); user.setList("11111"); ConcreteUser copyUser; try { copyUser = user.deepClone(); copyUser.setList("22222"); System.out.println(user.getList()); } catch(Exception e) { System.out.println("克隆失败..."); } }
结果:原型对象数据还是11111,没有被破坏,这就是深拷贝。
总结:
可以这样理解:如果修改克隆对象,原型对象的数据被破坏,就是浅拷贝;否则就是深拷贝;
3、Clone和final
浅拷贝时候,原型对象的clone与对象内的final关键字是有冲突的,下面看下简单的例子。
嗯,还是原来的配方,只不过在声明属性的时候声明为final,可以看到编译器报了斜体部分错误,final类型的不可重新赋值,所以在使用clone方法时,注意类的成员变量不要增加final关键字。