避免对象的浅拷贝,推荐使用序列化实现对象的拷贝

我们知道一个类实现了Cloneable接口就表示它具备了被拷贝的能力,如果再覆写clone()方法就会完全具备拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快很多,特别是在大对象的生成上,这会使性能的提升非常显著。但是对象拷贝也有一个比较容易忽略的问题:浅拷贝(Shadow Clone,也叫做影子拷贝)存在对象属性拷贝不彻底的问题。我们来看这样一段代码:

 
 
  1. public class Client {  
  2.      public static void main(String[] args) {  
  3.           //定义父亲  
  4.           Person f = new Person("父亲");  
  5.           //定义大儿子  
  6.           Person s1 = new Person("大儿子",f);  
  7.           //小儿子的信息是通过大儿子拷贝过来的  
  8.           Person s2 = s1.clone();  
  9.           s2.setName("小儿子");  
  10.           System.out.println(s1.getName() +" 的父亲是 " + s1.getFather().getName());  
  11.           System.out.println(s2.getName() +" 的父亲是 " + s2.getFather().getName());  
  12.      }  
  13. }  
  14.  
  15. class Person implements Cloneable{  
  16.      //姓名  
  17.      private String name;  
  18.      //父亲  
  19.      private Person father;  
  20.  
  21.      public Person(String _name){  
  22.           name = _name;  
  23.      }  
  24.      public Person(String _name,Person _parent){  
  25.           name = _name;  
  26.           father = _parent;  
  27.      }  
  28.      /*name和parent的getter/setter方法省略*/  
  29.  
  30.      //拷贝的实现  
  31.      @Override  
  32.      public Person clone(){  
  33.           Person p = null;  
  34.           try {  
  35.             p = (Person) super.clone();  
  36.           } catch (CloneNotSupportedException e) {  
  37.             e.printStackTrace();  
  38.           }  
  39.           return p;  
  40.     }  
程序中,我们描述了这样一个场景:一个父亲,有两个儿子,大小儿子同根同种,所以小儿子对象就通过拷贝大儿子对象来生成,运行输出的结果如下:
 
 
  1. 大儿子 的父亲是 父亲  
  2. 小儿子 的父亲是 父亲 
这很正确,没有问题。突然有一天,父亲心血来潮想让大儿子去认个干爹,也就是大儿子的父亲名称需要重新设置一下,代码如下:
 
 
  1. public static void main(String[] args) {  
  2.      //定义父亲  
  3.      Person f = new Person("父亲");  
  4.      //定义大儿子  
  5.      Person s1 = new Person("大儿子",f);  
  6.      //小儿子的信息是通过大儿子拷贝过来的  
  7.      Person s2 = s1.clone();  
  8.      s2.setName("小儿子");  
  9.      //认干爹  
  10.      s1.getFather().setName("干爹");  
  11.      System.out.println(s1.getName() +" 的父亲是 " + s1.getFather().getName());  
  12.      System.out.println(s2.getName() +" 的父亲是 " + s2.getFather().getName());  
上面仅仅修改了加粗字体部分,大儿子重新设置了父亲名称,我们期望的输出是:将大儿子父亲的名称修改为干爹,小儿子的父亲名称保持不变。下面来检查一下结果是否如此:
 
 
  1. 大儿子 的父亲是 干爹  
  2. 小儿子 的父亲是 干爹 

怎么回事,小儿子的父亲也成了“干爹”?两个儿子都没有,岂不是要气死“父亲”了!出现这个问题的原因就在于clone方法,我们知道所有类都继承自Object,Object提供了一个对象拷贝的默认方法,即上面代码中的super.clone方法,但是该方法是有缺陷的,它提供的是一种浅拷贝方式,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝,它的拷贝规则如下:

(1)基本类型

如果变量是基本类型,则拷贝其值,比如int、float等。

(2)对象

如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。这在Java中是很疯狂的,因为它突破了访问权限的定义:一个private修饰的变量,竟然可以被两个不同的实例对象访问,这让Java的访问权限体系情何以堪!

(3)String字符串

这个比较特殊,拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池(String Pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。(有关字符串的知识详见第4章。)

明白了这三个规则,上面的例子就很清晰了,小儿子对象是通过拷贝大儿子产生的,其父亲都是同一个人,也就是同一个对象,大儿子修改了父亲名称,小儿子也就跟着修改了—于是,父亲的两个儿子都没了!其实要更正也很简单,clone方法的代码如下:

 
 
  1. public Person clone(){  
  2.      Person p = null;  
  3.      try {  
  4.         p = (Person) super.clone();  
  5.         p.setFather(new Person(p.getFather().getName()));  
  6.      } catch (CloneNotSupportedException e) {  
  7.         e.printStackTrace();  
  8.      }  
  9.      return p;  

然后再运行,小儿子的父亲就不会是“干爹”了。如此就实现了对象的深拷贝(Deep Clone),保证拷贝出来的对象自成一体,不受“母体”的影响,和new生成的对象没有任何区别。

注意 浅拷贝只是Java提供的一种简单拷贝机制,不便于直接使用。






推荐使用序列化实现对象的拷贝

上面建议说了对象的浅拷贝问题,实现Cloneable接口就具备了拷贝能力,那我们来思考这样一个问题:如果一个项目中有大量的对象是通过拷贝生成的,那我们该如何处理?每个类都写一个clone方法,并且还要深拷贝?想想看这是何等巨大的工作量呀,是否有更好的方法呢?

其实,可以通过序列化方式来处理,在内存中通过字节流的拷贝来实现,也就是把母对象写到一个字节流中,再从字节流中将其读出来,这样就可以重建一个新对象了,该新对象与母对象之间不存在引用共享的问题,也就相当于深拷贝了一个新对象,代码如下:

 
 
  1. public class CloneUtils {  
  2.      // 拷贝一个对象  
  3.      @SuppressWarnings("unchecked")  
  4.      public static <T extends Serializable>  T clone(T obj) {  
  5.           // 拷贝产生的对象  
  6.           T clonedObj = null;  
  7.           try {  
  8.             // 读取对象字节数据  
  9.             ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  10.             ObjectOutputStream oos = new ObjectOutputStream(baos);  
  11.             oos.writeObject(obj);  
  12.             oos.close();  
  13.             // 分配内存空间,写入原始对象,生成新对象  
  14.             ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());  
  15.             ObjectInputStream ois = new ObjectInputStream(bais);  
  16.             //返回新对象,并做类型转换  
  17.             clonedObj = (T)ois.readObject();  
  18.             ois.close();  
  19.          } catch (Exception e) {  
  20.             e.printStackTrace();  
  21.          }  
  22.          return clonedObj;  
  23.      }  
此工具类要求被拷贝的对象必须实现Serializable接口,否则是没办法拷贝的(当然,使用反射那是另外一种技巧),上一个建议中的例子只要稍微修改一下即可实现深拷贝,代码如下:
 
 
  1. class Person implements Serializable{  
  2.      private static final long serialVersionUID = 1611293231L;  
  3.      /*删除掉clone方法,其他代码保持不变*/  

上去的,然后我们就可以通过CloneUtils工具进行对象的深拷贝了。用此方法进行对象拷贝时需要注意两点:

(1)对象的内部属性都是可序列化的

如果有内部属性不可序列化,则会抛出序列化异常,这会让调试者很纳闷:生成一个对象怎么会出现序列化异常呢?从这一点来考虑,也需要把CloneUtils工具的异常进行细化处理。

(2)注意方法和属性的特殊修饰符

比如final、static变量的序列化问题会被引入到对象拷贝中来(参考第1章),这点需要特别注意,同时transient变量(瞬态变量,不进行序列化的变量)也会影响到拷贝的效果。

当然,采用序列化方式拷贝时还有一个更简单的办法,即使用Apache下的commons工具包中的SerializationUtils类,直接使用更加简洁方便。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值