友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
第三电子书 返回本书目录 加入书签 我的书架 我的书签 TXT全本下载 『收藏到我的浏览器』

Java编程思想第4版[中文版](PDF格式)-第53部分

快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!


 (特别是收尾工作)可能不会针对任何特定的对象发生,所以为了强制采取这一行动, 

System。runFinalizersOnExit(true)添加了额外的开销,以保证收尾工作的正常进行。若没有基础类初始 

化,则输出结果是:  

  

not finalizing bases  

Creating Characteristic is alive  

LivingCreature()  

Creating Characteristic has heart  

Animal()  

Creating Characteristic can live in water  



                                                                                             201 


…………………………………………………………Page 203……………………………………………………………

Amphibian()  

Frog()  

bye!  

Frog finalize  

finalizing Characteristic is alive  

finalizing Characteristic has heart  

finalizing Characteristic can live in water  

  

从中可以看出确实没有为基础类Frog 调用收尾模块。但假如在命令行加入“finalize”自变量,则会获得下 

述结果:  

  

Creating Characteristic is alive  

LivingCreature()  

Creating Characteristic has heart  

Animal()  

Creating Characteristic can live in water  

Amphibian()  

Frog()  

bye!  

Frog finalize  

Amphibian finalize  

Animal finalize  

LivingCreature finalize  

finalizing Characteristic is alive  

finalizing Characteristic has heart  

finalizing Characteristic can live in water  

  

尽管成员对象按照与它们创建时相同的顺序进行收尾,但从技术角度说,并没有指定对象收尾的顺序。但对 

于基础类,我们可对收尾的顺序进行控制。采用的最佳顺序正是在这里采用的顺序,它与初始化顺序正好相 

反。按照与 C++中用于“破坏器”相同的形式,我们应该首先执行衍生类的收尾,再是基础类的收尾。这是 

由于衍生类的收尾可能调用基础类中相同的方法,要求基础类组件仍然处于活动状态。因此,必须提前将它 

们清除(破坏)。  



7。7。3  构建器内部的多形性方法的行为  



构建器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。 

若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况 

呢?在原始的方法内部,我们完全可以想象会发生什么——动态绑定的调用会在运行期间进行解析,因为对 

象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。为保持一致性,大家也许会 

认为这应该在构建器内部发生。  

但实际情况并非完全如此。若调用构建器内部一个动态绑定的方法,会使用那个方法被覆盖的定义。然而, 

产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。  

从概念上讲,构建器的职责是让对象实际进入存在状态。在任何构建器内部,整个对象可能只是得到部分组 

织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用 

却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。如果在构建器内部做这件 

事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。  

通过观察下面这个例子,这个问题便会昭然若揭:  

  

//: PolyConstructors。java  

// Constructors and polymorphism  

// don't produce what you might expect。  

  

abstract class Glyph {  



                                                                                     202 


…………………………………………………………Page 204……………………………………………………………

  abstract void draw();  

  Glyph()  {  

    System。out。println(〃Glyph() before draw()〃);  

    draw();   

    System。out。println(〃Glyph() after draw()〃);  

  }  

}  

  

class RoundGlyph extends Glyph {  

  int radius = 1;  

  RoundGlyph(int r) {  

    radius = r;  

    System。out。println(  

      〃RoundGlyph。RoundGlyph(); radius = 〃  

      + radius);  

  }  

  void draw() {   

    System。out。println(  

      〃RoundGlyph。draw(); radius = 〃 + radius);  

  }  

}  

  

public class PolyConstructors {  

  public static void main(String'' args) {  

    new RoundGlyph(5);  

  }  

} ///:~  

  

在Glyph 中,draw()方法是“抽象的”(abstract ),所以它可以被其他方法覆盖。事实上,我们在 

RoundGlyph 中不得不对其进行覆盖。但Glyph 构建器会调用这个方法,而且调用会在RoundGlyph。draw()中 

止,这看起来似乎是有意的。但请看看输出结果:  

  

Glyph() before draw()  

RoundGlyph。draw(); radius = 0  

Glyph() after draw()  

RoundGlyph。RoundGlyph(); radius = 5  

  

当Glyph 的构建器调用draw()时,radius 的值甚至不是默认的初始值1,而是 0。这可能是由于一个点号或 

者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原 

因。  

前一节讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的:  

(1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。  

(2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的draw()方法会得到调用(的确是在 

RoundGlyph 构建器调用之前),此时会发现 radius 的值为 0,这是由于步骤(1)造成的。  

(3) 按照原先声明的顺序调用成员初始化代码。  

(4) 调用衍生类构建器的主体。  

  

采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价 

的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象句柄。如果假若忘记初始 

化那个句柄,就会在运行期间出现违例事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的 

警告信号。  

在另一方面,应对这个程序的结果提高警惕。从逻辑的角度说,我们似乎已进行了无懈可击的设计,所以它 



                                                                                           203 


…………………………………………………………Page 205……………………………………………………………

的错误行为令人非常不可思议。而且没有从编译器那里收到任何报错信息(C++在这种情况下会表现出更合理 

的行为)。象这样的错误会很轻易地被人忽略,而且要花很长的时间才能找出。  

因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调 

用任何方法。在构建器内唯一能够安全调用的是在基础类中具有final 属性的那些方法(也适用于private 

方法,它们自动具有final 属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。  



7。8 通过继承进行设计  



学习了多形性的知识后,由于多形性是如此“聪明”的一种工具,所以看起来似乎所有东西都应该继承。但 

假如过度使用继承技术,也会使自己的设计变得不必要地复杂起来。事实上,当我们以一个现成类为基础建 

立一个新类时,如首先选择继承,会使情况变得异常复杂。  

一个更好的思路是首先选择“合成”——如果不能十分确定自己应使用哪一个。合成不会强迫我们的程序设 

计进入继承的分级结构中。同时,合成显得更加灵活,因为可以动态选择一种类型(以及行为),而继承要 

求在编译期间准确地知道一种类型。下面这个例子对此进行了阐释:  

  

//: Transmogrify。java  

// Dynamically changing the behavior of  

// an object via position。  

  

interface Actor {  

  void act();  

}  

  

class HappyActor implements Actor {  

  public void act() {   

    System。out。println(〃HappyActor〃);   

  }  

}  

  

class SadActor implements Actor {  

  public void act() {   

    System。out。println(〃SadActor〃);  

  }  

}  

  

class Stage {  

  Actor a = new HappyActor();  

  void change() { a = new SadActor(); }  

  void go() { a。act(); }  

}  

  

public class Transmogrify {  

  public static void main(String'' args) {  

    Stage s = new Stage();  

    s。go(); // Prints 〃HappyActor〃  

    s。change();  

    s。go(); // Prints 〃SadActor〃  

  }  

} ///:~  

  

在这里,一个Stage 对象包含了指向一个Actor 的句柄,后者被初始化成一个 HappyActor 对象。这意味着 

go()会产生特定的行为。但由于句柄在运行期间可以重新与一个不同的对象绑定或结合起来,所以SadActor 

对象的句柄可在a 中得到替换,然后由go()产生的行为发生改变。这样一来,我们在运行期间就获得了很大 



                                                                                           204 


…………………………………………………………Page 206……………………………………………………………

的灵活性。与此相反,我们不能在运行期间换用不同的形式来进行继承;它要求在编译期间完全决定下来。  

一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。在上述例子中,两者都 

得到了应用:继承了两个不同的类,用于表达 act()方法的差异;而 Stage 通过合成技术允许它自己的状态 

发生变化。在这种情况下,那种状态的改变同时也产生了行为的变化。  



7。8。1  纯继承与扩展  



学习继承时,为了创建继承分级结构,看来最明显的方法是采取一种“纯粹”的手段。也就是说,只有在基 

础类或“接口”中已建立的方法才可在衍生类中被覆盖,如下面这张图所示:  

  



                                        

  

可将其描述成一种纯粹的“属于”关系,因为一个类的接口已规定了它到底“是什么”或者“属于什么”。 

通过继承,可保证所有衍生类都只拥有基础类的接口。如果按上述示意图操作,衍生出来的类除了基础类的 

接口之外,也不会再拥有其他什么。  

可将其想象成一种“纯替换”,因为衍生类对象可为基础类完美地替换掉。使用它们的时候,我们根本没必 

要知道与子类有关的任何额外信息。如下所示:  

  



                                             

  

也就是说,基础类可接收我们发给衍生类的任何消息,因为两者拥有完全一致的接口。我们要做的全部事情 

就是从衍生上溯造型,而且永远不需要回过头来检查对象的准确类型是什么。所有细节都已通过多形性获得 

了完美的控制。  

若按这种思路考虑问题,那么一个纯粹的“属于”关系似乎是唯一明智的设计方法,其他任何设计方法都会 

导致混乱不清的思路,而且在定义上存在很大的困难。但这种想法又属于另一个极端。经过细致的研究,我 

们发现扩展接口对于一些特定问题来说是特别有效的方案。可将其称为“类似于”关系,因为扩展后的衍生 

类“类似于”基础类——它们有相同的基础接口——但它增加了一些特性,要求用额外的方法加以实现。如 

下所示:  

  



                                                                       205 


…………………………………………………………Page 207……………………………………………………………

                               

  

尽管这是一种有用和明智的做法(由具体的环境决定),但它也有一个缺点:衍生类中对接口扩展的那一部 

分不可在基础类中使用。所以一旦上溯造型,就不可再调用新方法:  

  



                                            

  

若在此时不进行上溯造型,则不会出现此类问题。但在许多情况下,都需要重新核实对象的准确类型,使自 

己能访问那个类型的扩展方法。在后面的小节里,我们具体讲述了这是如何实现的。  



7。8。2  下溯造型与运行期类型标识  



由于我们在上溯造型(在继承结构中向上移动)期间丢失了具体的类型信息,所以为了获取具体的类型信 

息——亦即在分级结构中向下移动——我们必须使用  “下溯造型”技术。然而,我们知道一个上溯造型肯定 

是安全的;基础类不可能再拥有一个比衍生类更大的接口。因此,我们通过基础类接口发送的每一条消息都 

肯定能够接收到。但在进行下溯造型的时候,我们(举个例子来说)并不真的知道一个几何形状实际是一个 

圆,它完全可能是一个三角形、方形或者其他形状。  

  



                                                                             206 


…………………………………………………………Page 208……………………………………………………………

                                                  

  

为解决这个问题,必须有一种办法能够保证下溯造型正确进行。只有这样,我们才不会冒然造型成一种错误 

的类型,然后发出一条对象不可能收到的消息。这样做是非常不安全的。  

在某些语言中(如 C++),为了进行保证“类型安全”的下溯造型,必须采取特殊的操作。但在Java 中,所 

有造型都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧造型,进入运行期以后,仍然会毫 

无留情地对这个造型进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个 

ClassCastException (类造型违例)。在运行期间对类型进行检查的行为叫作“运行期类型标识” 

 (RTTI )。下面这个例子向大家演示了RTTI 的行为:  

  

//: RTTI。java  

// Downcasting & Run…Time Type  

/
返回目录 上一页 下一页 回到顶部 1 1
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!