在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序——即无论在项目最初创建时还是在需要添加新功能时都可以“生长”的程序。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。这种类型的组织机制对那些拥有过过程化程序设计背景的人来说,更容易理解。而多态的作用则是消除类型之间的耦合关系。在前一章中,我们已经知道,继承允许将对象视为它自己本身的类型或其基类型来加以处理。这种能力极其重要,因为它允许将多种类型视为同一类型来处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出而来的。这种区别是根据方法行为的不同而表示出来的,虽然这些方法都可以通过同一基类来调用。
8.1 再论向上转型
我们已经知道,对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。而这种把对某个对象的引用视为对其基类型的引用的做法被称为向上转型。
publicenumNote { MIDDLE_C, C_SHARP, B_FLAT; // Etc. } ///:~ classInstrument { publicvoidplay(Noten) { print("Instrument.play()"); } } ///:~ //: polymorphism/music/Wind.java packagepolymorphism.music; // Wind objects are instruments // because they have the same interface: publicclassWindextendsInstrument { // Redefine interface method: publicvoidplay(Noten) { System.out.println("Wind.play() "+n); } } classStringedextendsInstrument { publicvoidplay(Noten) { print("Stringed.play() "+n); } } classBrassextendsInstrument { publicvoidplay(Noten) { print("Brass.play() "+n); } } ///:~ //: polymorphism/music/Music.java // Inheritance & upcasting. packagepolymorphism.music; publicclassMusic { publicstaticvoidtune(Instrumenti) { // ... i.play(Note.MIDDLE_C); } publicstaticvoidmain(String[] args) { Windflute=newWind(); tune(flute); // Upcasting } }
/* Output:
Wind.play() MIDDLE_C
*///:~
请观察一下tune()方法:
publicstaticvoidtune(Instrumenti) { // ... i.play(Note.MIDDLE_C); }
它接受一个Instrument引用,那么在这种情况下,编译器怎样才能知道这个Instrument引用指向的是Wind对象,而不是Brass对象或Stringed对象呢?实际上编译器无法得知。为了深入理解这个问题,有必要研究一下绑定这个话题。
8.2.1 方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定(如果有的话,有编译器和连接程序实现),叫做前期绑定。
上述程序之所以令人迷惑,主要是因为前期绑定。因为,当编译器只有一个Instrument引用时,它无法知道究竟调用哪个方法才对。
解决的方法就是后期绑定,它的含义就是在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定或运行时绑定。如果一个语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要想一下就会知道,不管怎么样都必须在对象中安置某种“类型信息”。
Java中除了static方法和final方法(private方法属于final方法)之外,其他所有方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定——它会自动发生。
为什么要将某个方法声明为final方法呢?正如前一章提到的那样,它可以防止其他人覆盖该方法。但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者说,告诉编译器不需要对其进行动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。然而,大多数情况下,这样做对程序的整体性能不会有什么改观。所以最好根据设计来决定是否使用final,而不是出于试图提高性能的目的来使用final。
8.2.2 产生正确的行为
一旦知道Java中所有的方法都是通过动态绑定实现多态这个事实以后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。或者换一种说法,发送消息给每个对象,让该对象去断定应该做什么事。
面向对象程序设计中,有一个经典的例子就是“几何形状”(shape)。因为它很直观,所以经常用到;但不幸的是,它可能使初学者认为面向对象程序设计仅适用于图形化程序设计,实际当然不是这样。
在“几何形状”这个例子中,有一个基类Shape,以及多个导出类——如Circle、Square、Triangle等。这个例子之所以好用,是因为我们可以说“圆是一种几何形状”、这种说法也很容易被理解。下面的继承图展示它们之间的关系:
向上转型可以像下面这条语句这么简单:
Shape s = new Circle();
这里,创建一个Circle对象,并把得到的引用立即赋值给了Shape,这样做看似错误;但实际上没有问题,因为通过继承,Circle就是一种Shape。因此,编译器认可这条语句,也就不会产生错误信息。
假设你调用一个基类方法(他已在导出类中被覆盖):
s.draw();
你可能在此认为调用的是Shape的draw(),因为这毕竟是一个Shape引用,那么编译器是怎样知道去做其他的事情呢?由于后期绑定(多态),还是正确调用了Circle.draw()方法。
8.2.3 可扩展性
由于有多态机制,我们可以根据自己的需求对系统添加人一多的新类型,而不需要修改方法。在一个设计良好的OOP程序中,大多数或者所有的方法都会遵循多态模型,只与基类接口同学。这样的程序是可扩展的,因为可以从通用的积累继承出新的数据类型,从而新添一些功能。那些操作基类接口的方法不需要任何改动就可以应用于新类。