`
TonyLian
  • 浏览: 403261 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

【第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

 

1
0
分享到:
评论

相关推荐

    CLONE 10-ex

    标签“KYO CLONE 10”可能是这个工具的特定品牌或者开发者,"KYO"可能是一个公司或者开发团队的名称,他们专注于克隆技术,并且已经发展到了第10代产品。 由于提供的压缩文件名为“Clone10-EX-LV2”,我们可以推测...

    java Clone

    Java中的`clone`方法是Java语言提供的一种复制对象的方式,它允许创建一个对象的副本,这个副本与原对象具有相同的属性值,但它们是两个独立的对象,修改副本不会影响原对象。`clone`方法存在于Java的`java.lang....

    clone()示例源码

    至于文件列表中的"lib",这通常表示库文件或者依赖项,可能包含了一些用于演示`clone()`方法使用的第三方库或者Java标准库。在实际项目中,这些库文件可以帮助开发者完成更多的功能,例如日志记录、单元测试等。 ...

    java_clone用法

    ### Java中的`clone`方法详解:浅拷贝与深拷贝 #### 一、引言 在Java中,`clone`方法提供了一种快速复制对象的方式。它属于`Object`类的一部分,但需要显式地在子类中声明并实现`Cloneable`接口才能正常使用。本文...

    java clone

    理解其工作原理,以及何时和如何正确地使用`clone`,对于提升代码质量和效率至关重要。在实践中,根据具体需求选择合适的复制策略,如浅拷贝、深拷贝,或者使用其他构造方式,是提高代码健壮性的关键。

    java clone的小例子

    首先,`clone()`方法是Java Object类的一个成员方法,这意味着所有Java类都隐式地继承了这个方法。它的主要作用是创建并返回当前对象的一个副本,也就是浅拷贝。浅拷贝意味着新创建的对象拥有原始对象的属性值,但...

    git代码clone,submodule

    ### Git Clone与Submodule知识点详解 #### 一、Git Clone命令 `git clone` 命令用于克隆一个远程仓库到本地。当你需要获取一个项目的全部文件时,这个命令非常有用。 **基本用法:** ```bash git clone ``` **...

    Clone详解.doc

    - 重写`clone()`方法时,需要处理非基本类型的成员变量,确保它们也能够被正确地复制。 总的来说,Java中的克隆机制提供了复制对象的能力,这在很多场景下都非常有用,比如数据备份、并发操作、状态恢复等。但需要...

    git clone 最新版

    "git clone"是Git中的一个核心命令,用于复制远程仓库到本地。在本文中,我们将深入探讨`git clone`命令以及如何获取其最新版本。 首先,让我们了解`git clone`的基本用法。当你运行`git clone <repository>`时,它...

    Jlink-clone解决办法,替换文件.rar

    在IT行业中,Jlink通常指的是SEGGER公司的J-Link系列编程和调试接口,它广泛用于嵌入式系统开发,特别是针对ARM架构的设备。当遇到"Jlink-clone...在尝试此类解决方案时,保持谨慎并做好备份,以避免不必要的损失。

    jlink v9 warning clone解决

    `jlink v9 warning clone`问题通常涉及到JLink版本9在与MDK配合使用时遇到的警告,提示可能与克隆设备或非法设备相关。 标题中的"jlink v9 warning clone解决"意味着开发者正在尝试解决关于JLink v9版本出现的克隆...

    windows git 批量 clone 脚本

    在Windows环境中,Git批量操作是开发团队...通过熟练掌握以上知识点,你就能创建出高效且可维护的批处理脚本来管理多个Git仓库,极大地提高工作效率。记得根据实际需求对这些脚本进行个性化调整,以适应你的工作流程。

    MinesweeperClone汉化版

    Minesweeper CLone 0.97 Saolei.net扫雷网,小门汉化 Men Shiyun Mine.exe 请自行下载MinesweeperClone_0.97.exe安装后使用

    jquery.clone.js

    jquery.clone

    Java clone方法使用

    详细的描述了Java中 clone方法使用

    Jlink V8固件升级提示Clone的解决方法!

    在使用高版本版KEIL时,提示要升级固件,升级后就出现JLINK is Clone的提示!“the emulator is JLink-Clone, the segger software only support orginal segger device” 然后闪退,IDE崩溃关闭! 解决方案: 1....

    clone-voice.zip

    "clone-voice.zip" 是一个压缩包文件,很可能包含了与语音克隆技术相关的代码或工具。根据提供的标签 "python",我们可以推测这个项目是使用Python编程语言实现的。Python在处理音频数据和人工智能领域有着广泛的...

    Java深浅clone

    在Java编程语言中,`Cloneable`接口和`clone()`方法是两个重要的概念,它们用于对象复制。在本文中,我们将深入探讨Java中的浅克隆(shallow clone)和深克隆(deep clone),并结合测试代码进行分析。 首先,让...

    中文语音克隆内含数据集和预训练模型:voice clone.zip

    用户可能只需要提供少量的个人语音样本,模型就能调整参数,以尽可能接近地模仿该人的发音和语调。 在"voice clone.zip"中,预训练模型可能已经过优化,可以处理中文语音的特殊性,如声调变化对意义的影响。使用者...

    matlab开发-MinesweeperClone

    matlab开发-MinesweeperClone。用于Matlab的Microsoft Windows XP扫雷器克隆

Global site tag (gtag.js) - Google Analytics