按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
// Fill up the array with shapes:
for(int i = 0; i 《 s。length; i++)
s'i' = randShape();
164
…………………………………………………………Page 166……………………………………………………………
// Make polymorphic method calls:
for(int i = 0; i 《 s。length; i++)
s'i'。draw();
}
} ///:~
针对从 Shape 衍生出来的所有东西,Shape 建立了一个通用接口——也就是说,所有(几何)形状都可以描
绘和删除。衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。
在主类 Shapes 里,包含了一个static 方法,名为 randShape()。它的作用是在每次调用它时为某个随机选
择的Shape 对象生成一个句柄。请注意上溯造型是在每个 return 语句里发生的。这个语句取得指向一个
Circle,Square 或者Triangle 的句柄,并将其作为返回类型 Shape 发给方法。所以无论什么时候调用这个
方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的 Shape 句柄。
main()包含了 Shape 句柄的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道
自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,
并为每个元素调用 draw()的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的
那样:
Circle。draw()
Triangle。draw()
Circle。draw()
Circle。draw()
Circle。draw()
Square。draw()
Triangle。draw()
Square。draw()
Square。draw()
当然,由于几何形状是每次随机选择的,所以每次运行都可能有不同的结果。之所以要突出形状的随机选
择,是为了让大家深刻体会这一点:为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。
对 draw()的所有调用都是通过动态绑定进行的。
7。2。3 扩展性
现在,让我们仍然返回乐器(Instrument)示例。由于存在多形性,所以可根据自己的需要向系统里加入任
意多的新类型,同时毋需更改 true()方法。在一个设计良好的 OOP 程序中,我们的大多数或者所有方法都会
遵从 tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础
类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法
根本不需要改变,
对于乐器例子,假设我们在基础类里加入更多的方法,以及一系列新类,那么会出现什么情况呢?下面是示
意图:
165
…………………………………………………………Page 167……………………………………………………………
所有这些新类都能与老类——tune()默契地工作,毋需对tune()作任何调整。即使 tune()位于一个独立的文
件里,而将新方法添加到 Instrument 的接口,tune()也能正确地工作,不需要重新编译。下面这个程序是对
上述示意图的具体实现:
//: Music3。java
// An extensible program
import java。util。*;
class Instrument3 {
public void play() {
System。out。println(〃Instrument3。play()〃);
}
public String what() {
return 〃Instrument3〃;
}
public void adjust() {}
}
class Wind3 extends Instrument3 {
public void play() {
System。out。println(〃Wind3。play()〃);
}
public String what() { return 〃Wind3〃; }
public void adjust() {}
}
class Percussion3 extends Instrument3 {
public void play() {
166
…………………………………………………………Page 168……………………………………………………………
System。out。println(〃Percussion3。play()〃);
}
public String what() { return 〃Percussion3〃; }
public void adjust() {}
}
class Stringed3 extends Instrument3 {
public void play() {
System。out。println(〃Stringed3。play()〃);
}
public String what() { return 〃Stringed3〃; }
public void adjust() {}
}
class Brass3 extends Wind3 {
public void play() {
System。out。println(〃Brass3。play()〃);
}
public void adjust() {
System。out。println(〃Brass3。adjust()〃);
}
}
class Woodwind3 extends Wind3 {
public void play() {
System。out。println(〃Woodwind3。play()〃);
}
public String what() { return 〃Woodwind3〃; }
}
public class Music3 {
// Doesn't care about type; so new types
// added to the system still work right:
static void tune(Instrument3 i) {
// 。。。
i。play();
}
static void tuneAll(Instrument3'' e) {
for(int i = 0; i 《 e。length; i++)
tune(e'i');
}
public static void main(String'' args) {
Instrument3'' orchestra = new Instrument3'5';
int i = 0;
// Upcasting during addition to the array:
orchestra'i++' = new Wind3();
orchestra'i++' = new Percussion3();
orchestra'i++' = new Stringed3();
orchestra'i++' = new Brass3();
orchestra'i++' = new Woodwind3();
tuneAll(orchestra);
}
167
…………………………………………………………Page 169……………………………………………………………
} ///:~
新方法是what()和adjust() 。前者返回一个String 句柄,同时返回对那个类的说明;后者使我们能对每种
乐器进行调整。
在main()中,当我们将某样东西置入Instrument3数组时,就会自动上溯造型到 Instrument3。
可以看到,在围绕 tune()方法的其他所有代码都发生变化的同时,tune()方法却丝毫不受它们的影响,依然
故我地正常工作。这正是利用多形性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响
的部分造成影响。此外,我们认为多形性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有
发生改变的东西区分开”。
7。3 覆盖与过载
现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法play()的接口会在被覆盖的过
程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器允许我们对方法进行过
载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子:
//: WindError。java
// Accidentally changing the interface
class NoteX {
public static final int
MIDDLE_C = 0; C_SHARP = 1; C_FLAT = 2;
}
class InstrumentX {
public void play(int NoteX) {
System。out。println(〃InstrumentX。play()〃);
}
}
class WindX extends InstrumentX {
// OOPS! Changes the method interface:
public void play(NoteX n) {
System。out。println(〃WindX。play(NoteX n)〃);
}
}
public class WindError {
public static void tune(InstrumentX i) {
// 。。。
i。play(NoteX。MIDDLE_C);
}
public static void main(String'' args) {
WindX flute = new WindX();
tune(flute); // Not the desired behavior!
}
} ///:~
这里还向大家引入了另一个易于混淆的概念。在 InstrumentX 中,play()方法采用了一个 int (整数)数
值,它的标识符是NoteX。也就是说,即使NoteX 是一个类名,也可以把它作为一个标识符使用,编译器不
会报告出错。但在WindX 中,play()采用一个NoteX 句柄,它有一个标识符 n。即便我们使用“play(NoteX
NoteX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖play()的功能,但对方法的类
型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“过载”,而非“覆盖”。请仔细体
168
…………………………………………………………Page 170……………………………………………………………
会这两个术语的区别。“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都
只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java 命名规范,自变量标
识符就应该是noteX,这样可把它与类名区分开。
在 tune 中,“InstrumentX i ”会发出play()消息,同时将某个 NoteX 成员作为自变量使用(MIDDLE_C)。
由于NoteX 包含了 int 定义,过载的play()方法的 int 版本会得到调用。同时由于它尚未被“覆盖”,所以
会使用基础类版本。
输出是:
InstrumentX。play()
7。4 抽象类和方法
在我们所有乐器(Instrument)例子中,基础类 Instrument 内的方法都肯定是“伪”方法。若去调用这些方
法,就会出现错误。那是由于 Instrument 的意图是为从它衍生出去的所有类都创建一个通用接口。