设计模式之原型模式

来源:互联网 发布:axure for mac 汉化 编辑:程序博客网 时间:2024/06/05 15:28

在JavaEE开发中经常用到Spring框架,而在Spring框架中,在创建JavaBean时,Spring默认是单例的(即singleton),而有时候我们需要Bean是非单例的,尤其是和Struts2整合时的Action,Action必须是多例的,此时我们就会将Action配置成原型(即prototype)。在Java中创建对象我们经常通过new关键字来进行创建,实际上通过new关键字来创建对象在底层是一个非常耗时的过程,因为在这个过程中需要准备大量的数据来创建对象,那么当我们碰到需要大量使用一个对象的情形时,再通过new关键字来创建对象就是一个不太明智的选择了,在很大程度上会影响我们的程序性能,那么该怎么解决这种问题了?这就需要用到设计模式中的原型模式了,也就是对象的克隆(也可以说是对象的复制)。

在java中要实现对象的克隆,通常有两种实现方式:

①:实现Cloneable接口,利用Object的clone()方法来实现对象的复制;

②:利用对象的序列化和反序列化技术来实现对象的复制。


下面分别就这两种方式用代码来实现一下。

clone()方法实现克隆

要想通过clone()方法实现对象的克隆,那么该对象所属的类必须实现Cloneable接口,然后在类中重写clone()方法。注意,这里的重写clone()方法并不是指重写Cloneable接口中的方法,而是重写Object类中clone()方法,Cloneable接口是一个空接口,看以看看java中的源码截图。利用clone方法实现对象的克隆,其底层技术使用c语言技术实现的,在Object类中,clone方法被native修饰符修饰了,说明调用的是本地的方法。

Java的源码截图


Object类:


那么我们该如何具体的使用clone()方法来实现克隆呢?接下来先看一下浅度复制,代码如下:

浅度复制

package com.tiantang.prototype;import java.util.Date;public class Person implements Cloneable{// 姓名private String name;// 生日private Date birth;public String getName() {return name;}public void setName(String name) {this.name = name;}public Date getBirth() {return birth;}public void setBirth(Date birth) {this.birth = birth;}public Person(){}public Person(String name, Date birth) {super();this.name = name;this.birth = birth;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}}
package com.tiantang.prototype;import java.util.Date;/** * 原型模式(实现Cloneable接口,利用clone方法进行浅度复制) * @author LiuJinkun * */public class Client {public static void main(String[] args) throws Exception {Person p=new Person("AA",new Date(1234569873659L));//克隆Person p2=(Person) p.clone();//打印结果:p和p2的hash值不一样,但它们的属性值一样System.out.println("p的hash值"+p);System.out.println("p2的hash值"+p2);System.out.println("p的名字-->"+p.getName());System.out.println("p的生日-->"+p.getBirth());System.out.println("p2的名字-->"+p2.getName());System.out.println("p2的生日-->"+p2.getBirth());}}
代码的打印结果如图:


从打印的结果来看,发现p和p2的hash值不一样,但它们的属性值是一样的,说明我们完成了对象的克隆。但这种克隆方法会有一点小问题什么问题呢?代码如下

package com.tiantang.prototype;import java.util.Date;public class Person implements Cloneable{// 姓名private String name;// 生日private Date birth;public String getName() {return name;}public void setName(String name) {this.name = name;}public Date getBirth() {return birth;}public void setBirth(Date birth) {this.birth = birth;}public Person(){}public Person(String name, Date birth) {super();this.name = name;this.birth = birth;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}}
package com.tiantang.prototype;import java.util.Date;/** * 原型模式(实现Cloneable接口,利用clone浅度复制产生的问题) * @author LiuJinkun * */public class Client2 {public static void main(String[] args) throws Exception {Date birth=new Date(1234569873659L);Person p=new Person("AA",birth);//克隆Person p2=(Person) p.clone();//修改前的打印结果System.out.println("p的生日-->"+p.getBirth());System.out.println("p2的生日-->"+p2.getBirth());//修改p的生日birth.setTime(2114569873659L);System.out.println("修改后:******************");//修改后的打印结果System.out.println("p的生日-->"+p.getBirth());System.out.println("p2的生日-->"+p2.getBirth());//根据打印的结果发现p和p2的生日还是一样的}}
通过和之前的代码对比,我们发现Person类一模一样,只是修改了Client类的代码,但运行结果却大不一样:


从结果中发现,当我先克隆对象之后,然后再修改原对象p的birth属性值,发现克隆的对象p2的属性值也跟着改变了,为什么呢?而且我们克隆的要求是需要克隆完成之后,克隆对象的属性值和原对象的属性值不能再有关联啊,这种情况显然不满足我们的要求。这就是之前提到过的浅度复制存在的小问题!怎么解决呢?要想解决问题,我们得先明白问题是怎样产生的!

看看内存关系图,p对象中有个Date型的属性birth,当为p对象的birth属性赋值时,在内存图上会有p对象的属性指向birth对象,当我们复制p对象的时候,产生的克隆对象p2也会有一个箭头指向birth对象,因此p和p2的birth属性都指向了同一个birth对象,当birth的属性值改变时,p和p2的属性都会改变,而且始终相同,这就造成了上面克隆对象的属性会跟随原对象的属性改变。


而我们克隆的要求是要求克隆对象不随原对象改变,即要实现如下的关系图:



即当我们克隆p的时候,也要将Date型的birth对象也克隆一份,让克隆对象p2自己重新指向一个克隆birth对象,这样就不会导致原对象p改变而使克隆对象p2改变了,具体的代码实现如下:

package com.tiantang.prototype;import java.util.Date;public class Person2 implements Cloneable{// 姓名private String name;// 生日private Date birth;public String getName() {return name;}public void setName(String name) {this.name = name;}public Date getBirth() {return birth;}public void setBirth(Date birth) {this.birth = birth;}public Person2(){}public Person2(String name, Date birth) {super();this.name = name;this.birth = birth;}@Overrideprotected Object clone() throws CloneNotSupportedException {//复制对象Person2 p=(Person2)super.clone();//属性也复制p.birth=(Date) this.birth.clone();return p;}}
通过比较发现,我们只修改了clone()方法中的代码,在用测试代码运行结果看看:

package com.tiantang.prototype;import java.util.Date;/** * 原型模式(实现Cloneable接口,利用clone方法进行深度复制) * @author LiuJinkun * */public class Client3 {public static void main(String[] args) throws Exception {Date birth=new Date(1234569873659L);Person2 p=new Person2("AA",birth);//克隆Person2 p2=(Person2) p.clone();//修改前的打印结果System.out.println("p的生日-->"+p.getBirth());System.out.println("p2的生日-->"+p2.getBirth());//修改p的生日birth.setTime(2114569873659L);System.out.println("修改后:******************");//修改后的打印结果System.out.println("p的生日-->"+p.getBirth());System.out.println("p2的生日-->"+p2.getBirth());//根据打印的结果发现p和p2的生日不一样了}}
运行结果截图:



从运行结果中发现,当改变原对象p的birth属性时,不会再改变克隆对象p2的birth属性了,这说明它们分别指向了不同的birth对象。

序列化和反序列化实现原型模式

序列化和反序列化实现对象的克隆的原理是:先利用对象输出流(ObjectOutputStream)将对象输出到一个内存或者磁盘中,然后再利用对象输入流从内存或者磁盘中利用反序列化操作将对象读取出来,这样就实现了对象的克隆。但要注意一个问题,要想进行序列化和反序列化,则该对象必须实现java.io.Serializable接口,如果该对象含有复杂的类型的属性(例如自定义的类),则该属性所属的类也需要实现java.io.Serializable接口。(后面会有一篇博客会对此进行举例)。

具体的代码实现如下:

package com.tiantang.prototype;import java.io.Serializable;import java.util.Date;public class Person3 implements Serializable{private static final long serialVersionUID = 1L;// 姓名private String name;// 生日private Date birth;public String getName() {return name;}public void setName(String name) {this.name = name;}public Date getBirth() {return birth;}public void setBirth(Date birth) {this.birth = birth;}public Person3(){}public Person3(String name, Date birth) {super();this.name = name;this.birth = birth;}}

package com.tiantang.prototype;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.util.Date;/** * 原型模式(序列化与反序列化实现对象的深度复制) * @author LiuJinkun * */public class Client4 {public static void main(String[] args) throws Exception {Date birth=new Date(1234569873659L);Person3 p=new Person3("AA",birth);System.out.println("p的hash值-->"+p);System.out.println("p的名字-->"+p.getName());System.out.println("p的生日"+p.getBirth());//将对象写到字节数组输出流中ByteArrayOutputStream baos=new ByteArrayOutputStream();ObjectOutputStream oos=new ObjectOutputStream(baos);oos.writeObject(p);//将字节数组流中的内容写到字节数组中byte[] bytes=baos.toByteArray();oos.close();baos.close();//读取字节数组中的内容ByteArrayInputStream bais=new ByteArrayInputStream(bytes);ObjectInputStream ois=new ObjectInputStream(bais);Person3 p2=(Person3) ois.readObject();ois.close();bais.close();System.out.println("p2的hash值-->"+p2);System.out.println("p2的名字-->"+p2.getName());System.out.println("p2的生日"+p2.getBirth());}}

代码运行的结果:


利用序列化和反序列化技术实现的克隆是深度复制,不会出现clone()方法中出现浅度复制的问题,读者可以自行写代码测试一下。这一种克隆方式涉及到较多的io流的知识,如果看的这里的读者对io流技术不太了解的话,建议回头多复习复习,笔者感觉这一块的知识还是比较重要的,毕竟在JavaWeb开发过程中,经常会涉及到文件的上传与下载。

在文章开头就曾说过,为什么要用原型模式,直接用new关键字多创建几次对象不就可以了吗?何必这么费劲用对象的克隆呢?那是因为在频繁使用大量的相同的对象时,用new关键字创建对象相对缓慢,程序性能较差,因为用new创建对象时需要准备许多数据才能开始创建对象,现在笔者在下面写了一段代码,用来模拟这种效果,并比较new创建和克隆创建的性能谁更优。(由于new创建需要耗时准备数据,因此笔者在这里为了模拟出这种效果,就采用当new创建时让线程sleep()1毫秒)。

代码如下:

package com.tiantang.prototype;import java.util.Date;public class Person4 implements Cloneable {public Person4() {}public Person4(String name, Date birth) {try {//让线程沉睡1毫秒Thread.sleep(1);this.name = name;this.birth = birth;} catch (Exception e) {e.printStackTrace();}}// 姓名private String name;// 生日private Date birth;public String getName() {return name;}public void setName(String name) {this.name = name;}public Date getBirth() {return birth;}public void setBirth(Date birth) {this.birth = birth;}@Overrideprotected Object clone() throws CloneNotSupportedException {// 复制对象Person4 p = (Person4) super.clone();// 属性也复制p.birth = (Date) this.birth.clone();return p;}}

package com.tiantang.prototype;import java.util.Date;public class Client5 {public static void main(String[] args) throws Exception {Client5 c=new Client5();c.testNew(10000);c.testClone(10000);}/** * 测试采用new关键字创建n个对象所耗的时间 * @param times 要重复创建多少个对象的个数 */public void testNew(int times){long start=System.currentTimeMillis();for(int i=0;i<times;i++){Person4 p=new Person4("AA",new Date());}long end=System.currentTimeMillis();System.out.println("new关键字创建"+times+"个对象耗时-->:"+(end-start)+"毫秒");}/** * 测试采用克隆的方式创建n个对象所耗的时间 * 要重复创建多少个对象的个数 * @param times * @throws Exception */public void testClone(int times) throws Exception{long start=System.currentTimeMillis();Person4 p=new Person4("AA",new Date());for(int i=0;i<times;i++){Person4 p2=(Person4) p.clone();}long end=System.currentTimeMillis();System.out.println("采用克隆创建"+times+"个对象耗时-->:"+(end-start)+"毫秒");}}

笔者用自己电脑最后测试的结果如下:


通过比较可以很明显的发现,克隆创建对象明显优于通过new关键字创建对象。(说明:事实上,在测试时不需要让线程sleep()1毫秒,由于笔者的电脑配置比较低,所以不让线程sleep也能发现克隆创建相对比较快,但如果电脑的配置比较高,运行速度快的话,可能就观察不到这种效果了。)

原型模式基本上就这么多知识点了,最后总结一下,当通过new产生一个对象需要准备很多数据时,并且经常创建该对象时,可以采用原型模式来解决,这样性能上就会有较大的提升。当然原型模式经常需要与工厂模式结合使用,例如spring框架里bean创建action类时。

最后,笔者在前面提到序列化与反序列化,关于这个只是点,笔者会在后面的一篇文章里再进行详细的举例介绍。


1 0
原创粉丝点击