对于面向对象的程序设计语言多型性是第三种最基本的特征(前两种是数据抽象和继承
多形性(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来亦即实现了是什么与怎样做两个模块的分离利用多形性的概念代码的组织以及可读性均能获得改善此外还能创建易于扩展的程序无论在项目的创建过程中还是在需要加入新特性的时候它们都可以方便地成长
通过合并各种特征与行为封装技术可创建出新的数据类型通过对具体实施细节的隐藏可将接口与实施细节分离使所有细节成为private(私有)这种组织方式使那些有程序化编程背景人感觉颇为舒适但多形性却涉及对类型的分解通过上一章的学习大家已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待这种能力是十分重要的因为多个类型(从相同的基础类型中衍生出来)可被当作同一种类型对待而且只需一段代码即可对所有不同的类型进行同样的处理利用具有多形性的方法调用一种类型可将自己与另一种相似的类型区分开只要它们都是从相同的基础类型中衍生出来的这种区分是通过各种方法在行为上的差异实现的可通过基础类实现对那些方法的调用
在这一章中大家要由浅入深地学习有关多形性的问题(也叫作动态绑定推迟绑定或者运行期绑定)同时举一些简单的例子其中所有无关的部分都已剥除只保留与多形性有关的代码
上溯造型
在第章大家已知道可将一个对象作为它自己的类型使用或者作为它的基础类型的一个对象使用取得一个对象句柄并将其作为基础类型句柄使用的行为就叫作上溯造型——因为继承树的画法是基础类位于最上方
但这样做也会遇到一个问题如下例所示(若执行这个程序遇到麻烦请参考第章的小节赋值)
//: Musicjava
// Inheritance & upcasting
package c;
class Note {
private int value;
private Note(int val) { value = val; }
public static final Note
middleC = new Note()
cSharp = new Note()
cFlat = new Note();
} // Etc
class Instrument {
public void play(Note n) {
Systemoutprintln(Instrumentplay());
}
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
Systemoutprintln(Windplay());
}
}
public class Music {
public static void tune(Instrument i) {
//
iplay(NotemiddleC);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting
}
} ///:~
其中方法Musictune()接收一个Instrument句柄同时也接收从Instrument衍生出来的所有东西当一个Wind句柄传递给tune()的时候就会出现这种情况此时没有造型的必要这样做是可以接受的Instrument里的接口必须存在于Wind中因为Wind是从Instrument里继承得到的从Wind向Instrument的上溯造型可能缩小那个接口但不可能把它变得比Instrument的完整接口还要小
为什么要上溯造型
这个程序看起来也许显得有些奇怪为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时就可能产生这方面的疑惑而且如果让tune()简单地取得一个Wind句柄将其作为自己的自变量使用似乎会更加简单直观得多但要注意假如那样做就需为系统内Instrument的每种类型写一个全新的tune()假设按照前面的推论加入Stringed(弦乐)和Brass(铜管)这两种Instrument(乐器)
//: Musicjava
// Overloading instead of upcasting
class Note {
private int value;
private Note(int val) { value = val; }
public static final Note
middleC = new Note()
cSharp = new Note()
cFlat = new Note();
} // Etc
class Instrument {
public void play(Note n) {
Systemoutprintln(Instrumentplay());
}
}
class Wind extends Instrument {
public void play(Note n) {
Systemoutprintln(Windplay());
}
}
class Stringed extends Instrument {
public void play(Note n) {
Systemoutprintln(Stringedplay());
}
}
class Brass extends Instrument {
public void play(Note n) {
Systemoutprintln(Brassplay());
}
}
public class Music {
public static void tune(Wind i) {
iplay(NotemiddleC);
}
public static void tune(Stringed i) {
iplay(NotemiddleC);
}
public static void tune(Brass i) {
iplay(NotemiddleC);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute); // No upcasting
tune(violin);
tune(frenchHorn);
}
} ///:~
这样做当然行得通但却存在一个极大的弊端必须为每种新增的Instrument类编写与类紧密相关的方法这意味着第一次就要求多得多的编程量以后假如想添加一个象tune()那样的新方法或者为Instrument添加一个新类型仍然需要进行大量编码工作此外即使忘记对自己的某个方法进行过载设置编译器也不会提示任何错误这样一来类型的整个操作过程就显得极难管理有失控的危险
但假如只写一个方法将基础类作为自变量或参数使用而不是使用那些特定的衍生类岂不是会简单得多?也就是说如果我们能不顾衍生类只让自己的代码与基础类打交道那么省下的工作量将是难以估计的
这正是多形性大显身手的地方然而大多数程序员(特别是有程序化编程背景的)对于多形性的工作原理仍然显得有些生疏
深入理解
对于Musicjava的困难性可通过运行程序加以体会输出是Windplay()这当然是我们希望的输出但它看起来似乎并不愿按我们的希望行事请观察一下tune()方法
public static void tune(Instrument i) {
//
iplay(NotemiddleC);
}
它接收Instrument句柄所以在这种情况下编译器怎样才能知道Instrument句柄指向的是一个Wind而不是一个Brass或Stringed呢?编译器无从得知为了深入了理解这个问题我们有必要探讨一下绑定这个主题
方法调用的绑定
将一个方法调用同一个方法主体连接到一起就称为绑定(Binding)若在程序运行以前执行绑定(由编译器和链接程序如果有的话)就叫作早期绑定大家以前或许从未听说过这个术语因为它在任何程序化语言里都是不可能的C编译器只有一种方法调用那就是早期绑定
上述程序最令人迷惑不解的地方全与早期绑定有关因为在只有一个Instrument句柄的前提下编译器不知道具体该调用哪个方法
解决的方法就是后期绑定它意味着绑定在运行期间进行以对象的类型为基础后期绑定也叫作动态绑定或运行期绑定若一种语言实现了后期绑定同时必须提供一些机制可在运行期间判断对象的类型并分别调用适当的方法也就是说编译器此时依然不知道对象的类型但方法调用机制能自己去调查找到正确的方法主体不同的语言对后期绑定的实现方法是有所区别的但我们至少可以这样认为它们都要在对象中安插某些特殊类型的信息
Java中绑定的所有方法都采用后期绑定技术除非一个方法已被声明成final这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的
为什么要把一个方法声明成final呢?正如上一章指出的那样它能防止其他人覆盖那个方法但也许更重要的一点是它可有效地关闭动态绑定或者告诉编译器不需要进行动态绑定这样一来编译器就可为final方法调用生成效率更高的代码
产生正确的行为
知道Java里绑定的所有方法都通过后期绑定具有多形性以后就可以相应地编写自己的代码令其与基础类沟通此时所有的衍生类都保证能用相同的代码正常地工作或者换用另一种方法我们可以将一条消息发给一个对象让对象自行判断要做什么事情
在面向对象的程序设计中有一个经典的形状例子由于它很容易用可视化的形式表现出来所以经常都用它说明问题但很不幸的是它可能误导初学者认为OOP只是为图形化编程设计的这种认识当然是错误的