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

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

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


 “再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接口,所以能够将其“上溯造型”为基础 

类。对于下一章要讲述的多形性问题,这一点是至关重要的。  

尽管继承在面向对象的程序设计中得到了特别的强调,但在实际启动一个设计时,最好还是先考虑采用合成 

技术。只有在特别必要的时候,才应考虑采用继承技术(下一章还会讲到这个问题)。合成显得更加灵活。 

但是,通过对自己的成员类型应用一些继承技巧,可在运行期准确改变那些成员对象的类型,由此可改变它 

们的行为。  

尽管对于快速项目开发来说,通过合成和继承实现的代码再生具有很大的帮助作用。但在允许其他程序员完 

全依赖它之前,一般都希望能重新设计自己的类结构。我们理想的类结构应该是每个类都有 自己特定的用 



                                                                                   158 


…………………………………………………………Page 160……………………………………………………………

途。它们不能过大(如集成的功能太多,则很难实现它的再生),也不能过小(造成不能由自己使用,或者 

不能增添新功能)。最终实现的类应该能够方便地再生。  



6。11 练习  



(1) 用默认构建器(空自变量列表)创建两个类:A 和 B,令它们自己声明自己。从A 继承一个名为 C 的新 

类,并在C 内创建一个成员B。不要为C 创建一个构建器。创建类C 的一个对象,并观察结果。  

(2) 修改练习 1,使A 和B 都有含有自变量的构建器,则不是采用默认构建器。为C 写一个构建器,并在C 

的构建器中执行所有初始化工作。  

(3) 使用文件Cartoon。java ,将Cartoon 类的构建器代码变成注释内容标注出去。解释会发生什么事情。  

(4) 使用文件Chess。java,将Chess 类的构建器代码作为注释标注出去。同样解释会发生什么。  



                                                                  159 


…………………………………………………………Page 161……………………………………………………………

                                  第 7 章  多形性  



  

 “对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。”  

  

 “多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与 

 “怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建 

 “易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成 

长”。  

通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施 

细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。 

但多形性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类 

型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可 

被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多形性的方 

法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这 

种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。  

在这一章中,大家要由浅入深地学习有关多形性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同 

时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多形性有关的代码。  



7。1 上溯造型  



在第6 章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得 

一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位 

于最上方。  

但这样做也会遇到一个问题,如下例所示(若执行这个程序遇到麻烦,请参考第 3 章的3。1。2 小节“赋 

值”):  

  

//: Music。java   

// Inheritance & upcasting  

package c07;  

  

class Note {  

  private int value;  

  private Note(int val) { value = val; }  

  public static final Note  

    middleC = new Note(0);   

    cSharp = new Note(1);  

    cFlat = new Note(2);  

} // Etc。  

  

class Instrument {  

  public void play(Note n) {  

    System。out。println(〃Instrument。play()〃);  

  }  

}  

  

// Wind objects are instruments  

// because they have the same interface:  

class Wind extends Instrument {  

  // Redefine interface method:  

  public void play(Note n) {  

    System。out。println(〃Wind。play()〃);  



                                                                                    160 


…………………………………………………………Page 162……………………………………………………………

  }  

}  

  

public class Music {  

  public static void tune(Instrument i) {  

    // 。。。  

    i。play(Note。middleC);  

  }  

  public static void main(String'' args) {  

    Wind flute = new Wind();  

    tune(flute); // Upcasting  

  }  

} ///:~  

  

其中,方法 Music。tune()接收一个 Instrument 句柄,同时也接收从 Instrument 衍生出来的所有东西。当一 

个Wind 句柄传递给 tune()的时候,就会出现这种情况。此时没有造型的必要。这样做是可以接受的; 

Instrument里的接口必须存在于Wind 中,因为Wind是从 Instrument 里继承得到的。从 Wind 向Instrument 

的上溯造型可能“缩小”那个接口,但不可能把它变得比 Instrument 的完整接口还要小。  



7。1。1  为什么要上溯造型  



这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就 

可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind 句柄,将其作为自己的自变量使用,似乎 

会更加简单、直观得多。但要注意:假如那样做,就需为系统内 Instrument 的每种类型写一个全新的 

tune()。假设按照前面的推论,加入 Stringed (弦乐)和Brass (铜管)这两种Instrument (乐器):  

  

//: Music2。java   

// Overloading instead of upcasting  

  

class Note2 {  

  private int value;  

  private Note2(int val) { value = val; }  

  public static final Note2  

    middleC = new Note2(0);   

    cSharp = new Note2(1);  

    cFlat = new Note2(2);  

} // Etc。  

  

class Instrument2 {  

  public void play(Note2 n) {  

    System。out。println(〃Instrument2。play()〃);  

  }  

}  

  

class Wind2 extends Instrument2 {  

  public void play(Note2 n) {  

    System。out。println(〃Wind2。play()〃);  

  }  

}  

  

class Stringed2 extends Instrument2 {  

  public void play(Note2 n) {  

    System。out。println(〃Stringed2。play()〃);  



                                                                                             161 


…………………………………………………………Page 163……………………………………………………………

  }  

}  

  

class Brass2 extends Instrument2 {  

  public void play(Note2 n) {  

    System。out。println(〃Brass2。play()〃);  

  }  

}  

  

public class Music2 {  

  public static void tune(Wind2 i) {  

    i。play(Note2。middleC);  

  }  

  public static void tune(Stringed2 i) {  

    i。play(Note2。middleC);  

  }  

  public static void tune(Brass2 i) {  

    i。play(Note2。middleC);  

  }  

  public static void main(String'' args) {  

    Wind2 flute = new Wind2();  

    Stringed2 violin = new Stringed2();  

    Brass2 frenchHorn = new Brass2();  

    tune(flute); // No upcasting  

    tune(violin);  

    tune(frenchHorn);  

  }  

} ///:~  

  

这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的 Instrument2类编写与类紧密相关的方 

法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为 

Instrument添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行过载设 

置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。  

但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得 

多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计 

的。  

这正是“多形性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多形性的工作 

原理仍然显得有些生疏。  



7。2 深入理解  



对于Music。java 的困难性,可通过运行程序加以体会。输出是Wind。play()。这当然是我们希望的输出,但 

它看起来似乎并不愿按我们的希望行事。请观察一下tune()方法:  

  

public static void tune(Instrument i) {  

// 。。。  

i。play(Note。middleC);  

}  

  

它接收 Instrument 句柄。所以在这种情况下,编译器怎样才能知道 Instrument句柄指向的是一个 Wind ,而 

不是一个Brass 或Stringed 呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定” 

这个主题。  



                                                                                           162 


…………………………………………………………Page 164……………………………………………………………

7。2。1  方法调用的绑定  



将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编 

译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何 

程序化语言里都是不可能的。C 编译器只有一种方法调用,那就是“早期绑定”。  

上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个 Instrument 句柄的前提下,编译器不知 

道具体该调用哪个方法。  

解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动 

态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象 

的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去 

调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为: 

它们都要在对象中安插某些特殊类型的信息。  

Java 中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定 

是否应进行后期绑定——它是自动发生的。  

为什么要把一个方法声明成final 呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重 

要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可 

为final 方法调用生成效率更高的代码。  



7。2。2  产生正确的行为  



知道Java 里绑定的所有方法都通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类 

沟通。此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消 

息发给一个对象,让对象自行判断要做什么事情。”  

在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常 

都用它说明问题。但很不幸的是,它可能误导初学者认为 OOP 只是为图形化编程设计的,这种认识当然是错 

误的。  

形状例子有一个基础类,名为 Shape;另外还有大量衍生类型:Circle (圆形),Square (方形), 

Triangle (三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。 

下面这幅继承图向我们展示了它们的关系:  

  



                                                 

  

上溯造型可用下面这个语句简单地表现出来:  

  

Shape s = new Circle();  

  

在这里,我们创建了Circle 对象,并将结果句柄立即赋给一个Shape。这表面看起来似乎属于错误操作(将 

一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle 属于Shape 的一种。因此编 

译器认可上述语句,不会向我们提示一条出错消息。  

当我们调用其中一个基础类方法时(已在衍生类里覆盖):  

s。draw();  

同样地,大家也许认为会调用Shape 的 draw(),因为这毕竟是一个Shape 句柄。那么编译器怎样才能知道该 



                                                                    163 


…………………………………………………………Page 165……………………………………………………………

做其他任何事情呢?但此时实际调用的是 Circle。draw() ,因为后期绑定已经介入(多形性)。  

下面这个例子从一个稍微不同的角度说明了问题:  

  

//: Shapes。java  

// Polymorphism in Java  

  

class Shape {   

  void draw() {}  

  void erase() {}   

}  

  

class Circle extends Shape {  

  void draw() {   

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