【第10条】谨慎地改写clone

    原作者在这一条上用了8页的篇幅,翻译版也有7页,足以说明这一条的重要性。我个人对此条的标注是重量级的5颗星!

 

    克隆——是一个很让人“感兴趣”而又“颇有争议”的话题,无论是在生物界还是在代码的世界中。

 

    Java通过实现Cloneable接口来“说明”一个类支持clone方法。所谓clone就是返回一个当前对象的副本,注意这里所返回的是一个复制品,虽然它的内容应该与原对象完全一致(这才叫克隆吗),但是它们的地址指针却是不同的,区别于简单的地址赋值(= 操作符)。

 

    Object中clone方法的定义是:

    protected native Object clone() throws CloneNotSupportedException;

 

   首先它是保护的,其次它是native(本地)的,也就是说它是通过其他语言编写的代码,是看不到源码的,最后它可能抛出CloneNotSupportedException,在类不支持克隆时。

 

    Object.clone() 采用了本地方法,通过其他语言予以实现,可能是为了提高性能,也可能是其他什么原因,我们就不深究了。我们还是看看Cloneable接口吧:

public interface Cloneable { 
}

 

  它其实是个空空的,具体方法一个也没有。那么它到底做了什么呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,则Object的clone方法返回该对象的逐域拷贝,否则的话抛出一个CloneNotSupportedException异常。这是接口的一种极端非典型的用法,也不值得效仿。

 

    那么既然实现了Cloneable接口后,就可以调用Object中的clone方法了(Cloneable接口改变了超类中一个受保护的方法的行为),那我们的目的不就达到了吗?干吗还要改写clone呢?

 

    Object的clone方法,只能逐域拷贝那些原语类型,对于类仅仅是地址赋值,换句话说,它只是逐域在做 = 操作。这样并不是完全的克隆,所以我们需要改写clone方法。

 

    Object中关于克隆的约定:

1) x.clone() != x ,将会为 true

2) x.clone().getClass() == x.getClass() ,将会为 true

3) x.clone().equals(x) , 将会为 true

    “将会为true”,但是这也不是一个绝对要求。拷贝往往会导致创建一个新实例,但同时也会要求拷贝内部的数据结构。这个过程中没有调用构造函数。

 

    “没有调用构造函数”和“x.clone().getClass() == x.getClass() ” 的综合导致结果就是:如果你改写一个非final类的clone方法,则应该返回一个通过调用super.clone而得到的对象(具体推到过程见书上的第40页)。这其实也相当于给我提供了一个改写clone方法的“处方”:

     不要使用构造函数来创建类,而是使用超类的clone方法。

 

     于是clone方法的改写模板可以是这样的:

public class MyClass{
........

    public Object clone() {
        MyClass v = null;
        try{
            v = (MyClass) super.clone();
            // 逐域克隆MyClass的非原生类型域
     } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError();
       }
      return v;
     }
}

 

    简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法改写clone。此方法首先调用super.clone,然后修正任何需要修正的域。通常情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并且要用指向新对象的引用代替原来指向这些对象的引用。虽然,这些内部拷贝操作往往可以通过递归地调用clone来完成,但这通常并不是最佳方法。通常原语类型和非可变对象时无需修改的,但也有例外情况,譬如,一些代表唯一ID的序列号或代表对象创建时间的域,虽然是原语类型或非可变对象,也要被修改。

 

    其实,只里面的一句“然后修正任何需要修正的域”,可非同小可。“这意味着要拷贝任何包含内部‘深层结构’的可变对象”,这可就复杂了。如果域是一个数组的话,需要新创建一个相同大小的数组,并对每一个数组元素递归地调用他们的clone方法。

 

    但是,不幸的事情发生了。我在实践clone的时候发现,Java的集合类型其实实现的只是“浅表克隆”,也就是说它们并没有“逐域递归克隆深层结构”。为什么呢?其实,如果你自己去实现一个数组的“深层克隆”就会发现,由于放入数组(集合)中的元素可能是各种类型,所以只能是Object,而Object本身是无法clone的。理由很简单,不是所有的类都实现了Cloneable接口,那么如果一个放入数组(集合)中的对象是一个没有实现Cloneable接口的类,那么就无从谈起对它的克隆了。所以,约定中说的是“这意味着要拷贝任何包含内部‘深层结构’的可变对象”而不是“这意味着要递归克隆任何包含内部‘深层结构’的可变对象”。注意用的词是“拷贝”而不是“克隆”,因为任何类型都可以“拷贝”,其方法就不仅限于“克隆”了。

 

     那么该如何进行一个没有实现Cloneable接口的对象的“拷贝”工作呢?我们来举一个 BigDecimal 的例子。在调用完超类的clone方法之后,就该修改这些需要被修改的域了。

v.varBigDecimal = new BigDecimal(this.varBigDecimal.toString());

 

由于BigDecimal既没有实参类型为BigDecimal的valueOf静态工厂方法,也没有实参类型为BigDecimal的构造函数,所以只能通过String型来做“传递”。

 

    那么对于域中包含数组、集合,而声明时又没有指定其元素类型的类(尤其是那些用来被继承的超类本来就无从知晓类型)来说,这里的“修改”方法将变得异常复杂,你必须去判断每个元素的类型,当然还要求尽量考虑到所有可能的类型,但这也不能保证日后使用过程中不会出现之前未知的类型。

 

    再加之,各集合类型的clone方法已经是写好的了,而且很不幸,它们并没有如上方法去处理,而只是对内部元素的引用赋值,如果你想实现一个严格的clone方法,而你的类当中又可能出现集合类型的域,那么很不幸,你只有逐一补足那么集合类型的clone方法了(在调用原有的clone方法后,逐一遍历其内部元素并克隆之,里面还有集合怎么办?$#%&$#% 晕倒~ 崩溃~)

 

    看到这,也许你对clone的看法也会和我一样,真的要这么复杂吗?是的,所以书中这一条的题目才是“谨慎地”改写clone。同时,书中也给我们指了出路——最好的方法是,提供某些其他途径来代替对象拷贝,或干脆不提供这样的能力。一个好的代替方法是“拷贝构造函数”

public MyClass(MyClass myClass);

 

或另一种微小变形——静态工厂方法

public static MyClass newInstance(MyClass myClass);

 

 

 下面再说一点本书以外的内容,首先是关于“深度”、“浅表”和“影子”拷贝。

 

    “影子”一词是我自己“创造”的,其实和其他人所谓的“浅表”大致是一个意思。而我所谓的“浅表”,不仅仅是没有完成对深层对象的拷贝,而且甚至连引用赋值也没有做的才叫“浅表”,而“影子”拷贝是进行了引用赋值的。那么“浅表”拷贝的意义在与何呢?由于它对那些类对象什么也没有做,还保留着“原来”的样子,所以它的用意是“保护”那些非原语类型的域。在clone中,由于副本对象是刚刚通过super.clone()创建的,所以那些非原语类型的域实际上应该已经完成了引用的赋值,所以“保护”变成极其有限度的“加速”。它的“保护”作用更重要的体现地是copyProperties。

 

     第二是,克隆的应用场景。

 

     正如很多人不喜欢克隆,或者说clone有着很多争议那样,“如果你的代码中经常要使用clone,那么可以错略地说,你的设计是有问题的”。那么什么时候使用clone呢?一个典型的案例是:当从服务端得到查询结果后,将结果显示到GUI之上的同时,将其clone一份并保存起来。当用户对GUI上的信息进行了编辑之后按下“保存”按钮想要保存时,这时调出来刚才保存的clone档对比一下,如果发现并没有区别(x.equals(y) == true),将提示用户没有可保存的修改。可能是用户改来改去后,结果又回到了当初,这种情况是较为常见的。

 

    而在服务端,创建一个对象的副本,然后将原对象送入某一方法,在这个方法执行过后,再去比较原对象和副本对象是否还equals,类似这样的设计显然是不好的。

 

    最后再来说一下实践中比较实用的clone的替代品:

1) 如果一个类是可序列化的(实现系列化),那么可以将其先序列化,然后再反序列化已得到其副本。如果对象很大,甚至可以序列化到磁盘上。

 

2) 更普遍的,可以将一个对象转为字符流,然后在进而转到副本对象中。不依赖于类必须可系列化,所以更通用。

 

3) 如果是JavaBean(就像我的情况),可以使用org.apache.commons.beanutils.BeanUtils.cloneBean静态方法

 

 

 

 

【Effective Java 学习笔记】系列连载专题请见:
http://tonylian.iteye.com/categories/64208

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值