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

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

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



二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,真不敢 

想象会在程序员里引发什么样的情绪。  

另一个值得注意的是Hashtable (散列表),它是另一个重要的标准类。该类没有采用任何final 方法。正 

如我们在本书其他地方提到的那样,显然一些类的设计人员与其他设计人员有着全然不同的素质(注意比较 

Hashtable 极短的方法名与Vecor 的方法名)。对类库的用户来说,这显然是不应该如此轻易就能看出的。 

一个产品的设计变得不一致后,会加大用户的工作量。这也从另一个侧面强调了代码设计与检查时需要很强 

的责任心。  



6。9 初始化和类装载  



在许多传统语言里,程序都是作为启动过程的一部分一次性载入的。随后进行的是初始化,再是正式执行程 

序。在这些语言中,必须对初始化过程进行慎重的控制,保证 static数据的初始化不会带来麻烦。比如在一 

个 static 数据获得初始化之前,就有另一个 static数据希望它是一个有效值,那么在 C++中就会造成问 

题。  

Java 则没有这样的问题,因为它采用了不同的装载方法。由于 Java 中的一切东西都是对象,所以许多活动 

变得更加简单,这个问题便是其中的一例。正如下一章会讲到的那样,每个对象的代码都存在于独立的文件 

中。除非真的需要代码,否则那个文件是不会载入的。通常,我们可认为除非那个类的一个对象构造完毕, 

否则代码不会真的载入。由于 static 方法存在一些细微的歧义,所以也能认为“类代码在首次使用的时候载 

入”。  

首次使用的地方也是static 初始化发生的地方。装载的时候,所有 static对象和 static代码块都会按照本 

来的顺序初始化(亦即它们在类定义代码里写入的顺序)。当然,static 数据只会初始化一次。  



6。9。1  继承初始化  



我们有必要对整个初始化过程有所认识,其中包括继承,对这个过程中发生的事情有一个整体性的概念。请 

观察下述代码:  

  

//: Beetle。java  

// The full process of initialization。  

  

class Insect {  

  int i = 9;  

  int j;  

  Insect() {  

    prt(〃i = 〃 + i + 〃; j = 〃 + j);  

    j = 39;  

  }  

  static int x1 =   

    prt(〃static Insect。x1 initialized〃);  

  static int prt(String s) {  

    System。out。println(s);  

    return 47;  

  }  

}  

  



                                                                              157 


…………………………………………………………Page 159……………………………………………………………

public class Beetle extends Insect {  

  int k = prt(〃Beetle。k initialized〃);  

  Beetle() {  

    prt(〃k = 〃 + k);  

    prt(〃j = 〃 + j);  

  }  

  static int x2 =  

    prt(〃static Beetle。x2 initialized〃);  

  static int prt(String s) {  

    System。out。println(s);  

    return 63;  

  }  

  public static void main(String'' args) {  

    prt(〃Beetle constructor〃);  

    Beetle b = new Beetle();  

  }  

} ///:~  

  

该程序的输出如下:  

  

static Insect。x initialized  

static Beetle。x initialized  

Beetle constructor  

i = 9; j = 0  

Beetle。k initialized  

k = 63  

j = 39  

  

对Beetle 运行Java 时,发生的第一件事情是装载程序到外面找到那个类。在装载过程中,装载程序注意它 

有一个基础类(即 extends 关键字要表达的意思),所以随之将其载入。无论是否准备生成那个基础类的一 

个对象,这个过程都会发生(请试着将对象的创建代码当作注释标注出来,自己去证实)。  

若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(此时是 

Insect)执行 static 初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类 

的初始化可能要依赖于对基础类成员的正确初始化。  

此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们 

的默认值,而将对象句柄设为null 。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完 

全可以用 super 来自行指定构建器调用(就象在Beetle()构建器中的第一个操作一样)。基础类的构建采用 

与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,实例变量会按本来的顺序得以初始化。最 

后,执行构建器剩余的主体部分。  



6。10 总结  



无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过合成来实 

现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使用。但如果想实现接口的 

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

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

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

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

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

们的行为。  

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

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



                                                                                   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。draw()〃);   

  }  

  void erase() {   

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