📄 chapter12.htm
字号:
if(myHandle instanceof Cloneable) // ...<br>
第二个原因是考虑到我们可能不愿所有对象类型都能克隆。所以Object.clone()会验证一个类是否真的是实现了Cloneable接口。若答案是否定的,则“掷”出一个CloneNotSupportedException违例。所以在一般情况下,我们必须将“implement
Cloneable”作为对克隆能力提供支持的一部分。<br>
<br>
12.2.4 成功的克隆<br>
理解了实现clone()方法背后的所有细节后,便可创建出能方便复制的类,以便提供了一个本地副本:<br>
<br>
550-551页程序<br>
<br>
不管怎样,clone()必须能够访问,所以必须将其设为public(公共的)。其次,作为clone()的初期行动,应调用clone()的基础类版本。这里调用的clone()是Object内部预先定义好的。之所以能调用它,是由于它具有protected(受到保护的)属性,所以能在衍生的类里访问。<br>
Object.clone()会检查原先的对象有多大,再为新对象腾出足够多的内存,将所有二进制位从原来的对象复制到新对象。这叫作“按位复制”,而且按一般的想法,这个工作应该是由clone()方法来做的。但在Object.clone()正式开始操作前,首先会检查一个类是否Cloneable,即是否具有克隆能力——换言之,它是否实现了Cloneable接口。若未实现,Object.clone()就掷出一个CloneNotSupportedException违例,指出我们不能克隆它。因此,我们最好用一个try-catch块将对super.clone()的调用代码包围(或封装)起来,试图捕获一个应当永不出现的违例(因为这里确实已实现了Cloneable接口)。<br>
在LocalCopy中,两个方法g()和f()揭示出两种参数传递方法间的差异。其中,g()演示的是按引用传递,它会修改外部对象,并返回对那个外部对象的一个引用。而f()是对自变量进行克隆,所以将其分离出来,并让原来的对象保持独立。随后,它继续做它希望的事情。甚至能返回指向这个新对象的一个句柄,而且不会对原来的对象产生任何副作用。注意下面这个多少有些古怪的语句:<br>
v = (MyObject)v.clone();<br>
它的作用正是创建一个本地副本。为避免被这样的一个语句搞混淆,记住这种相当奇怪的编码形式在Java中是完全允许的,因为有一个名字的所有东西实际都是一个句柄。所以句柄v用于克隆一个它所指向的副本,而且最终返回指向基础类型Object的一个句柄(因为它在Object.clone()中是那样被定义的),随后必须将其造型为正确的类型。<br>
在main()中,两种不同参数传递方式的区别在于它们分别测试了一个不同的方法。输出结果如下:<br>
<br>
552页程序<br>
<br>
大家要记住这样一个事实:Java对“是否等价”的测试并不对所比较对象的内部进行检查,从而核实它们的值是否相同。==和!=运算符只是简单地对比句柄的内容。若句柄内的地址相同,就认为句柄指向同样的对象,所以认为它们是“等价”的。所以运算符真正检测的是“由于别名问题,句柄是否指向同一个对象?”<br>
<br>
12.2.5 Object.clone()的效果<br>
调用Object.clone()时,实际发生的是什么事情呢?当我们在自己的类里覆盖clone()时,什么东西对于super.clone()来说是最关键的呢?根类中的clone()方法负责建立正确的存储容量,并通过“按位复制”将二进制位从原始对象中复制到新对象的存储空间。也就是说,它并不只是预留存储空间以及复制一个对象——实际需要调查出欲复制之对象的准确大小,然后复制那个对象。由于所有这些工作都是在由根类定义之clone()方法的内部代码中进行的(根类并不知道要从自己这里继承出去什么),所以大家或许已经猜到,这个过程需要用RTTI判断欲克隆的对象的实际大小。采取这种方式,clone()方法便可建立起正确数量的存储空间,并对那个类型进行正确的按位复制。<br>
不管我们要做什么,克隆过程的第一个部分通常都应该是调用super.clone()。通过进行一次准确的复制,这样做可为后续的克隆进程建立起一个良好的基础。随后,可采取另一些必要的操作,以完成最终的克隆。<br>
为确切了解其他操作是什么,首先要正确理解Object.clone()为我们带来了什么。特别地,它会自动克隆所有句柄指向的目标吗?下面这个例子可完成这种形式的检测:<br>
<br>
553-554页程序<br>
<br>
一条Snake(蛇)由数段构成,每一段的类型都是Snake。所以,这是一个一段段链接起来的列表。所有段都是以循环方式创建的,每做好一段,都会使第一个构建器参数的值递减,直至最终为零。而为给每段赋予一个独一无二的标记,第二个参数(一个Char)的值在每次循环构建器调用时都会递增。<br>
increment()方法的作用是循环递增每个标记,使我们能看到发生的变化;而toString则循环打印出每个标记。输出如下:<br>
<br>
554页中程序<br>
<br>
这意味着只有第一段才是由Object.clone()复制的,所以此时进行的是一种“浅层复制”。若希望复制整条蛇——即进行“深层复制”——必须在被覆盖的clone()里采取附加的操作。<br>
通常可在从一个能克隆的类里调用super.clone(),以确保所有基础类行动(包括Object.clone())能够进行。随着是为对象内每个句柄都明确调用一个clone();否则那些句柄会别名变成原始对象的句柄。构建器的调用也大致相同——首先构造基础类,然后是下一个衍生的构建器……以此类推,直到位于最深层的衍生构建器。区别在于clone()并不是个构建器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进行。<br>
<br>
12.2.6 克隆合成对象<br>
试图深层复制合成对象时会遇到一个问题。必须假定成员对象中的clone()方法也能依次对自己的句柄进行深层复制,以此类推。这使我们的操作变得复杂。为了能正常实现深层复制,必须对所有类中的代码进行控制,或者至少全面掌握深层复制中需要涉及的类,确保它们自己的深层复制能正确进行。<br>
下面这个例子总结了面对一个合成对象进行深层复制时需要做哪些事情:<br>
<br>
555-556页程序<br>
<br>
DepthReading和TemperatureReading非常相似;它们都只包含了基本数据类型。所以clone()方法能够非常简单:调用super.clone()并返回结果即可。注意两个类使用的clone()代码是完全一致的。<br>
OceanReading是由DepthReading和TemperatureReading对象合并而成的。为了对其进行深层复制,clone()必须同时克隆OceanReading内的句柄。为达到这个目标,super.clone()的结果必须造型成一个OceanReading对象(以便访问depth和temperature句柄)。<br>
<br>
12.2.7 用Vector进行深层复制<br>
下面让我们复习一下本章早些时候提出的Vector例子。这一次Int2类是可以克隆的,所以能对Vector进行深层复制:<br>
<br>
557-558页程序<br>
<br>
Int3自Int2继承而来,并添加了一个新的基本类型成员int j。大家也许认为自己需要再次覆盖clone(),以确保j得到复制,但实情并非如此。将Int2的clone()当作Int3的clone()调用时,它会调用Object.clone(),判断出当前操作的是Int3,并复制Int3内的所有二进制位。只要没有新增需要克隆的句柄,对Object.clone()的一个调用就能完成所有必要的复制——无论clone()是在层次结构多深的一级定义的。<br>
至此,大家可以总结出对Vector进行深层复制的先决条件:在克隆了Vector后,必须在其中遍历,并克隆由Vector指向的每个对象。为了对Hashtable(散列表)进行深层复制,也必须采取类似的处理。<br>
这个例子剩余的部分显示出克隆已实际进行——证据就是在克隆了对象以后,可以自由改变它,而原来那个对象不受任何影响。<br>
<br>
12.2.8 通过序列化进行深层复制<br>
若研究一下第10章介绍的那个Java 1.1对象序列化示例,可能发现若在一个对象序列化以后再撤消对它的序列化,或者说进行装配,那么实际经历的正是一个“克隆”的过程。<br>
那么为什么不用序列化进行深层复制呢?下面这个例子通过计算执行时间对比了这两种方法:<br>
<br>
559-560页程序<br>
<br>
其中,Thing2和Thing4包含了成员对象,所以需要进行一些深层复制。一个有趣的地方是尽管Serializable类很容易设置,但在复制它们时却要做多得多的工作。克隆涉及到大量的类设置工作,但实际的对象复制是相当简单的。结果很好地说明了一切。下面是几次运行分别得到的结果:<br>
的确<br>
561页上程序<br>
<br>
除了序列化和克隆之间巨大的时间差异以外,我们也注意到序列化技术的运行结果并不稳定,而克隆每一次花费的时间都是相同的。<br>
<br>
12.2.9 使克隆具有更大的深度<br>
若新建一个类,它的基础类会默认为Object,并默认为不具备克隆能力(就象在下一节会看到的那样)。只要不明确地添加克隆能力,这种能力便不会自动产生。但我们可以在任何层添加它,然后便可从那个层开始向下具有克隆能力。如下所示:<br>
<br>
561-562页程序<br>
<br>
添加克隆能力之前,编译器会阻止我们的克隆尝试。一旦在Scientist里添加了克隆能力,那么Scientist以及它的所有“后裔”都可以克隆。<br>
<br>
12.2.10 为什么有这个奇怪的设计<br>
之所以感觉这个方案的奇特,因为它事实上的确如此。也许大家会奇怪它为什么要象这样运行,而该方案背后的真正含义是什么呢?后面讲述的是一个未获证实的故事——大概是由于围绕Java的许多买卖使其成为一种设计优良的语言——但确实要花许多口舌才能讲清楚这背后发生的所有事情。<br>
最初,Java只是作为一种用于控制硬件的语言而设计,与因特网并没有丝毫联系。象这样一类面向大众的语言一样,其意义在于程序员可以对任意一个对象进行克隆。这样一来,clone()就放置在根类Object里面,但因为它是一种公用方式,因而我们通常能够对任意一个对象进行克隆。看来这是最灵活的方式了,毕竟它不会带来任何害处。<br>
正当Java看起来象一种终级因特网程序设计语言的时候,情况却发生了变化。突然地,人们提出了安全问题,而且理所当然,这些问题与使用对象有关,我们不愿望任何人克隆自己的保密对象。所以我们最后看到的是为原来那个简单、直观的方案添加的大量补丁:clone()在Object里被设置成“protected”。必须将其覆盖,并使用“implement
Cloneable”,同时解决违例的问题。<br>
只有在准备调用Object的clone()方法时,才没有必要使用Cloneable接口,因为那个方法会在运行期间得到检查,以确保我们的类实现了Cloneable。但为了保持连贯性(而且由于Cloneable无论如何都是空的),最好还是由自己实现Cloneable。<br>
<br>
12.3 克隆的控制<br>
为消除克隆能力,大家也许认为只需将clone()方法简单地设为private(私有)即可,但这样是行不通的,因为不能采用一个基础类方法,并使其在衍生类中更“私有”。所以事情并没有这么简单。此外,我们有必要控制一个对象是否能够克隆。对于我们设计的一个类,实际有许多种方案都是可以采取的:<br>
(1)
保持中立,不为克隆做任何事情。也就是说,尽管不可对我们的类克隆,但从它继承的一个类却可根据实际情况决定克隆。只有Object.clone()要对类中的字段进行某些合理的操作时,才可以作这方面的决定。<br>
(2) 支持clone(),采用实现Cloneable(可克隆)能力的标准操作,并覆盖clone()。在被覆盖的clone()中,可调用super.clone(),并捕获所有违例(这样可使clone()不“掷”出任何违例)。<br>
(3)
有条件地支持克隆。若类容纳了其他对象的句柄,而那些对象也许能够克隆(集合类便是这样的一个例子),就可试着克隆拥有对方句柄的所有对象;如果它们“掷”出了违例,只需让这些违例通过即可。举个例子来说,假设有一个特殊的Vector,它试图克隆自己容纳的所有对象。编写这样的一个Vector时,并不知道客户程序员会把什么形式的对象置入这个Vector中,所以并不知道它们是否真的能够克隆。<br>
(4) 不实现Cloneable(),但是将clone()覆盖成protected,使任何字段都具有正确的复制行为。这样一来,从这个类继承的所有东西都能覆盖clone(),并调用super.clone()来产生正确的复制行为。注意在我们实现方案里,可以而且应该调用super.clone()——即使那个方法本来预期的是一个Cloneable对象(否则会掷出一个违例),因为没有人会在我们这种类型的对象上直接调用它。它只有通过一个衍生类调用;对那个衍生类来说,如果要保证它正常工作,需实现Cloneable。<br>
(5) 不实现Cloneable来试着防止克隆,并覆盖clone(),以产生一个违例。为使这一设想顺利实现,只有令从它衍生出来的任何类都调用重新定义后的clone()里的suepr.clone()。<br>
(6) 将类设为final,从而防止克隆。若clone()尚未被我们的任何一个上级类覆盖,这一设想便不会成功。若已被覆盖,那么再一次覆盖它,并“掷”出一个CloneNotSupportedException(克隆不支持)违例。为担保克隆被禁止,将类设为final是唯一的办法。除此以外,一旦涉及保密对象或者遇到想对创建的对象数量进行控制的其他情况,应该将所有构建器都设为private,并提供一个或更多的特殊方法来创建对象。采用这种方式,这些方法就可以限制创建的对象数量以及它们的创建条件——一种特殊情况是第16章要介绍的singleton(独子)方案。<br>
<br>
下面这个例子总结了克隆的各种实现方法,然后在层次结构中将其“关闭”:<br>
<br>
564-565页程序<br>
<br>
第一个类Ordinary代表着大家在本书各处最常见到的类:不支持克隆,但在它正式应用以后,却也不禁止对其克隆。但假如有一个指向Ordinary对象的句柄,而且那个对象可能是从一个更深的衍生类上溯造型来的,便不能判断它到底能不能克隆。<br>
WrongClone类揭示了实现克隆的一种不正确途径。它确实覆盖了Object.clone(),并将那个方法设为public,但却没有实现Cloneable。所以一旦发出对super.clone()的调用(由于对Object.clone()的一个调用造成的),便会无情地掷出CloneNotSupportedException违例。<br>
在IsCloneable中,大家看到的才是进行克隆的各种正确行动:先覆盖clone(),并实现了Cloneable。但是,这个clone()方法以及本例的另外几个方法并不捕获CloneNotSupportedException违例,而是任由它通过,并传递给调用者。随后,调用者必须用一个try-catch代码块把它包围起来。在我们自己的clone()方法中,通常需要在clone()内部捕获CloneNotSupportedException违例,而不是任由它通过。正如大家以后会理解的那样,对这个例子来说,让它通过是最正确的做法。<br>
类NoMore试图按照Java设计者打算的那样“关闭”克隆:在衍生类clone()中,我们掷出CloneNotSupportedException违例。TryMore类中的clone()方法正确地调用super.clone(),并解析成NoMore.clone(),后者掷出一个违例并禁止克隆。<br>
但在已被覆盖的clone()方法中,假若程序员不遵守调用super.clone()的“正确”方法,又会出现什么情况呢?在BackOn中,大家可看到实际会发生什么。这个类用一个独立的方法duplicate()制作当前对象的一个副本,并在clone()内部调用这个方法,而不是调用super.clone()。违例永远不会产生,而且新类是可以克隆的。因此,我们不能依赖“掷”出一个违例的方法来防止产生一个可克隆的类。唯一安全的方法在ReallyNoMore中得到了演示,它设为final,所以不可继承。这意味着假如clone()在final类中掷出了一个违例,便不能通过继承来进行修改,并可有效地禁止克隆(不能从一个拥有任意继承级数的类中明确调用Object.clone();只能调用super.clone(),它只可访问直接基础类)。因此,只要制作一些涉及安全问题的对象,就最好把那些类设为final。<br>
在类CheckCloneable中,我们看到的第一个类是tryToClone(),它能接纳任何Ordinary对象,并用instanceof检查它是否能够克隆。若答案是肯定的,就将对象造型成为一个IsCloneable,调用clone(),并将结果造型回Ordinary,最后捕获有可能产生的任何违例。请注意用运行期类型鉴定(见第11章)打印出类名,使自己看到发生的一切情况。<br>
在main()中,我们创建了不同类型的Ordinary对象,并在数组定义中上溯造型成为Ordinary。在这之后的头两行代码创建了一个纯粹的Ordinary对象,并试图对其克隆。然而,这些代码不会得到编译,因为clone()是Object中的一个protected(受到保护的)方法。代码剩余的部分将遍历数组,并试着克隆每个对象,分别报告它们的成功或失败。输出如下:<br>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -