第7章多形性

整理文档很辛苦,赏杯茶钱您下走!

免费阅读已结束,点击下载阅读编辑剩下 ...

阅读已结束,您可以下载文档离线阅读编辑

资源描述

第7章多形性“对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。”“多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。但多形性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多形性的方法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。在这一章中,大家要由浅入深地学习有关多形性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多形性有关的代码。7.1上溯造型在第6章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。但这样做也会遇到一个问题,如下例所示(若执行这个程序遇到麻烦,请参考第3章的3.1.2小节“赋值”):252-253页程序其中,方法Music.tune()接收一个Instrument句柄,同时也接收从Instrument衍生出来的所有东西。当一个Wind句柄传递给tune()的时候,就会出现这种情况。此时没有造型的必要。这样做是可以接受的;Instrument里的接口必须存在于Wind中,因为Wind是从Instrument里继承得到的。从Wind向Instrument的上溯造型可能“缩小”那个接口,但不可能把它变得比Instrument的完整接口还要小。7.1.1为什么要上溯造型这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument的每种类型写一个全新的tune()。假设按照前面的推论,加入Stringed(弦乐)和Brass(铜管)这两种Instrument(乐器):253-254页程序这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为Instrument添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行过载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。这正是“多形性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多形性的工作原理仍然显得有些生疏。7.2深入理解对于Music.java的困难性,可通过运行程序加以体会。输出是Wind.play()。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。请观察一下tune()方法:publicstaticvoidtune(Instrumenti){//...i.play(Note.middleC);}它接收Instrument句柄。所以在这种情况下,编译器怎样才能知道Instrument句柄指向的是一个Wind,而不是一个Brass或Stringed呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。7.2.1方法调用的绑定将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C编译器只有一种方法调用,那就是“早期绑定”。上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个Instrument句柄的前提下,编译器不知道具体该调用哪个方法。解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。为什么要把一个方法声明成final呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为final方法调用生成效率更高的代码。7.2.2产生正确的行为知道Java里绑定的所有方法都通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类沟通。此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消息发给一个对象,让对象自行判断要做什么事情。”在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常都用它说明问题。但很不幸的是,它可能误导初学者认为OOP只是为图形化编程设计的,这种认识当然是错误的。形状例子有一个基础类,名为Shape;另外还有大量衍生类型:Circle(圆形),Square(方形),Triangle(三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。下面这幅继承图向我们展示了它们的关系:257页图上溯造型可用下面这个语句简单地表现出来:Shapes=newCircle();在这里,我们创建了Circle对象,并将结果句柄立即赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle属于Shape的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。当我们调用其中一个基础类方法时(已在衍生类里覆盖):s.draw();同样地,大家也许认为会调用Shape的draw(),因为这毕竟是一个Shape句柄。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是Circle.draw(),因为后期绑定已经介入(多形性)。下面这个例子从一个稍微不同的角度说明了问题:257-258页程序针对从Shape衍生出来的所有东西,Shape建立了一个通用接口——也就是说,所有(几何)形状都可以描绘和删除。衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。在主类Shapes里,包含了一个static方法,名为randShape()。它的作用是在每次调用它时为某个随机选择的Shape对象生成一个句柄。请注意上溯造型是在每个return语句里发生的。这个语句取得指向一个Circle,Square或者Triangle的句柄,并将其作为返回类型Shape发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的Shape句柄。main()包含了Shape句柄的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,并为每个元素调用draw()的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的那样:259页程序当然,由于几何形状是每次随机选择的,所以每次运行都可能有不同的结果。之所以要突出形状的随机选择,是为了让大家深刻体会这一点:为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。对draw()的所有调用都是通过动态绑定进行的。7.2.3扩展性现在,让我们仍然返回乐器(Instrument)示例。由于存在多形性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改true()方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变,对于乐器例子,假设我们在基础类里加入更多的方法,以及一系列新类,那么会出现什么情况呢?下面是示意图:260页图所有这些新类都能与老类——tune()默契地工作,毋需对tune()作任何调整。即使tune()位于一个独立的文件里,而将新方法添加到Instrument的接口,tune()也能正确地工作,不需要重新编译。下面这个程序是对上述示意图的具体实现:260-262页程序新方法是what()和adjust()。前者返回一个String句柄,同时返回对那个类的说明;后者使我们能对每种乐器进行调整。在main()中,当我们将某样东西置入Instrument3数组时,就会自动上溯造型到Instrument3。可以看到,在围绕tune()方法的其他所有代码都发生变化的同时,tune()方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多形性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多形性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。7.3覆盖与过载现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器允许我们对方法进行过载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子:263页程序这里还向大家引入了另一个易于混淆的概念。在InstrumentX中,play()方法采用了一个int(整数)数值,它的标识符是NoteX。也就是说,即使NoteX是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在WindX中,play()采用一个NoteX句柄,它有一个标识符n。即便我们使用“play(NoteXNoteX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖play()的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“过载”,而非“覆盖”。请仔细体会这两个术语的区别。“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java命名规范,自变量标识符就应该是noteX,这样可把它与类名区分开。在tune中,“InstrumentXi”会发出play()消息,同时将某个NoteX成员作为自变量使用(MIDDLE_C)。由于NoteX包含了

1 / 25
下载文档,编辑使用

©2015-2020 m.777doc.com 三七文档.

备案号:鲁ICP备2024069028号-1 客服联系 QQ:2149211541

×
保存成功