按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
…………………………………………………………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() {
System。out。println(〃Circle。erase()〃);
}
}
class Square extends Shape {
void draw() {
System。out。println(〃Square。draw()〃);
}
void erase() {
System。out。println(〃Square。erase()〃);
}
}
class Triangle extends Shape {
void draw() {
System。out。println(〃Triangle。draw ()〃);
}
void erase() {
System。out。println(〃Triangle。erase()〃);
}
}
public class Shapes {
public static Shape randShape() {
switch((int)(Math。random() * 3)) {
default: // To quiet the piler
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
public static void main(String'' args) {
Shape'' s = new Shape'9';
// Fill up the array with shapes:
for(int i = 0; i 《 s。length; i++)
s'i' = randShape();