java 注解 @?(name="username"),原型(Prototype)模式 != Object.clone()

本文介绍了原型模式在Java中的应用,澄清了对Cloneable接口的理解。通过邮寄快递的场景展示了原型模式的实用价值,详细阐述了浅拷贝和深拷贝的概念,提供了实现深拷贝的两种方法,并通过示例代码进行验证。原型模式简化了对象创建,特别是在处理复杂对象图时,提高了效率。
摘要由CSDN通过智能技术生成

5.1 概述

谈到原型模式,学过Java的人可能会想到java.lang.Cloneable这个接口,以为Java的原型模式描述的就是java.lang.Cloneable接口的使用,这就大错特错了。其实,原型模式在我们日常生活中经常可以看到,比如你刚给你的客厅做了装修,你朋友正好也希望给他的客厅做装修,那么,他可能会把你家的装修方案拿过来改改就成,你的装修方案就是原型。

由于很多OOP语言都支持对象的克隆(拷贝)以方便复制对象,但这些方式并不那么完美,后述我们将会讨论。

5.2 原型模式

当创建这些对象(一般情况是一些大对象)非常耗时,或者创建过程非常复杂时,非常有用,GoF给出的原型模式定义如下:

原型模式的静态类图非常简单,如下所示:

Client使用Prototype的clone()方法得到这个对象的拷贝,其实拷贝原型对象不一定是指从内存中进行拷贝,我们的原型数据可能保存在数据库里。

一般情况下,OOP语言都提供了内存中对象的复制,Java语言提供了对象的浅拷贝(Shallow copy),也就是说复制一个对象时,如果它的一个属性是引用,则复制这个引用,使之指向内存中同一个对象;但如果为此属性创建了一个新对象,让其引用指向它,即是深拷贝(Deep copy)。

5.3 寄个快递

下面是一个邮寄快递的场景:

“给我寄个快递。”顾客说。

“寄往什么地方?寄给……?”你问。

“和上次差不多一样,只是邮寄给另外一个地址,这里是邮寄地址……”顾客一边说一边把写有邮寄地址的纸条给你。

“好!”你愉快地答应,因为你保存了用户的以前邮寄信息,只要复制这些数据,然后通过简单的修改就可以快速地创建新的快递数据了。

5.4 实现

我们在复制新的数据时,需要特别注意的是,我们不能把所有数据都复制过来,例如,当对象包含主键时,不能使用原型数据的主键,必须创建一个新的主键。我们这里提供一个静态工厂方法,来获得原型数据,然后拷贝这些数据,最后做相应的初始化。为了操作安全起见,我们不直接使用原型数据,而是使用clone()方法从内存克隆这条原型数据做后续操作。我们使用Java提供java.lang.Cloneable接口克隆数据,它实现对象的浅拷贝,关于java.lang.Cloneable接口的使用请看后续介绍。

5.4.1 UML静态类图

Client使用PackageInfo提供的静态工厂方法clonePackage(String userName)创建一个新对象:首先根据userName加载一条用户以前的数据作为原型数据(可以是数据库,可以是其他任何你保存数据的地方),然后在内存中克隆这条数据,最后初始化该数据并返回。

5.4.2 代码实现

public class PackageInfo implements Cloneable {

//getters, setters and other methods...

public PackageInfo clone() {

try {

return (PackageInfo)super.clone();

} catch (CloneNotSupportedException e) {

System.out.println("Cloning not allowed.");

return null;

}

}

public static PackageInfo clonePackage(String userName) {

//load package as prototype data from db...

PackageInfo prototype = loadPackageInfo(userName);

//clone information ...

prototype = prototype.clone();

//initialize copied data ...

prototype.setId(null);

return prototype;

}

}

代码注解:

Java的java.lang.Object方法里就提供了克隆方法clone(),原则上似乎所有类都拥有此功能,但其实不然,关于它的使用有如下限制:

要实现克隆,必须实现java.lang.Cloneable接口,否则在运行时调用clone()方法,会抛CloneNotSupportedException异常。

返回的是Object类型的对象,所以使用时可能需要强制类型转换。

该方法是protected的,如果想让外部对象使用它,必须在子类重写该方法,设定其访问范围是public的,参见PackageInfo的clone()方法。

Object的clone()方法的复制是采用逐字节的方式从复制内存数据,复制了属性的引用,而属性所指向的对象本身没有被复制,因此所复制的引用指向了相同的对象。由此可见,这种方式拷贝对象是浅拷贝,不是深拷贝。

静态工厂方法public static PackageInfo clonePackage(String userName)方法根据原型创建一份拷贝:首先拿出用户以前的一条数据,即这句PackageInfo prototype = loadPackageInfo(userName),然后调用方法它的clone()方法完成内存拷贝,即prototype.clone(),最后我们初始化这条新数据,比如使id为空等。

现在来看看我们的测试代码,如下所示:

public class PackageInfoTestDrive {

public static void main(String[] args) {

PackageInfo currentInfo = PackageInfo.clonePackage("John");

System.out.println("Original package information:");

display(currentInfo);

currentInfo.setId(10000l);

currentInfo.setReceiverName("Ryan");

currentInfo.setReceiverAddress("People Square, Shanghai");

System.out.println("\nNew package information:");

display(currentInfo);

}

//other methods…

}

我们通过这句,PackageInfo currentInfo = PackageInfo.clonePackage("John"),拷贝了一份快递信息出来,通过设置currentInfo.setReceiverName("Ryan")和currentInfo.setReceiverAddress("People Square, Shanghai"),便完成了第二个包裹的信息录入,测试结果如下:

Original package information:

Package id: null

Receiver name: John

Receiver address: People Square,Shanghai

Sender name: William

Sender Phone No.: 12345678901

New package information:

Package id: 10000

Receiver name: Ryan

Receiver address: People Square, Shanghai

Sender name: William

Sender Phone No.: 12345678901

在实际的应用中,使用原型模式创建对象图 (Object Graph)非常便捷。

5.5 深拷贝(Deep Copy)

通过上述学习,我们知道Java提供了浅拷贝的方法,那么,如何实现一个深拷贝呢?一般情况下,我们有两种方式来实现:

1. 拷贝对象时,递归地调用属性对象的克隆方法完成。读者可以根据具体的类,撰写出实现特定类型的深拷贝方法。

一般地,我们很难实现一个一般性的方法来完成任何类型对象的深拷贝。有人根据反射得到属性的类型,然后依照它的类型构造对象,但前提是,这些属性的类型必须含有一个公有的默认构造方法,否则作为一个一般性的方法,很难确定传递给非默认构造方法的参数值;此外,如果属性类型是接口或者抽象类型,必须提供查找到相关的具体类方法,作为一个一般性的方法,这个也很难办到。

2. 如果类实现了java.io.Serializable接口,把原型对象序列化,然后反序列化后得到的对象,其实就是一个新的深拷贝对象。

我们整理给出第二种方法的实现,代码片段大致如下所示:

import java.io.Serializable;

//other imports…

public class DeepCopyBean implements Serializable {

private String objectField;

private int primitiveField;

//getters and setters …

public DeepCopyBean deepCopy() {

try {

ByteArrayOutputStream buf = new ByteArrayOutputStream();

ObjectOutputStream o = new ObjectOutputStream(buf);

o.writeObject(this);

ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));

return (DeepCopyBean) in.readObject();

} catch (IOException e) {

e.printStackTrace();

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

return null;

}

}

代码注解:

DeepCopyBean实现了java.io.Serializable接口,它含有一个原始类型(Primitive Type)的属性primitiveField和对象属性objectField。

此类的deepCopy()方法首先序列化自己到流中,然后从流中反序列化,得到的对象便是一个新的深拷贝。

为了验证是不是实现了深拷贝,我们编写了如下测试代码:

DeepCopyBean originalBean = new DeepCopyBean();

//create a String object in jvm heap not jvm string pool

originalBean.setObjectField(new String("123456"));

originalBean.setPrimitiveField(2);

//clone this bean

DeepCopyBean newBean = originalBean.deepCopy();

System.out.println("Primitive ==? " + (newBean.getPrimitiveField() == originalBean.getPrimitiveField()));

System.out.println("Object ==? " + (newBean.getObjectField() == originalBean.getObjectField()));

System.out.println("Object equal? " + (newBean.getObjectField().equals(originalBean.getObjectField())));

注意:

这句,originalBean.setObjectField(new String("123456"))和originalBean.setObjectField("123456")是不一样的,前者创建了两个String对象,其中一个是在JVM的字符串池(String pool)里,另外一个在堆中,并且属性引用指向的对象在堆里;后者属性引用指向了JVM字符串池中的"123456"对象。

如果是浅拷贝,即引用指向同一内存地址,则newBean.getObjectField() == originalBean.getObjectField()为true,如果是深拷贝,则创建了不同对象,引用指向的地址肯定不一样,即此值应为false。但是这两种方式,使用这句,newBean.getObjectField().equals(originalBean.getObjectField()),进行比较,其结果必须为true,测试结果如下所示:

Primitive ==? true

Object ==? false

Object equal? true

和我们预想的结果一样,原始类型的使用==进行比较,结果相等,而引用类型使用==比较,结果显示未指向相同的地址,但是使用equals()方法比较的结果为true,即证明我们实现了深拷贝。

使用这种方式进行深拷贝,一方面,它只能拷贝实现Serializable接口类型的对象,其属性也是可序列化的;另一方面,序列化和反序列化比较耗时。选用此方式实现深拷贝时需要做这两方面的权衡。

5.6 总结

我们以前使用java.lang.Cloneable的一个很大原因是使用new创建对象的速度相对来说比较慢,如今,随着JVM性能的提升,new的速度已经很接近Object的clone()方法的速度了,然而这并没有使原型模式使用失去多少光泽,使用原型模式有以下优点:

创建大的聚合对象图时,没必要为每个层次的子对象创建相应层次的工厂类。

方便实例化,只要复制对象,然后初始化对象,就可以得到你想要的对象,并不不需要过多的编程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值