您好,欢迎访问三七文档
第七章多态在面向对象的程序设计语言中,多态(polymorphic)是继数据抽象和继承之后的第三种基本特性。多态通过分离“做什么”和“怎么做”,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建“可扩展的”程序,即无论在项目最初创建时,还是在需要添加新功能时,都可以进行扩充。“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过细节“私有化(private)”将接口和实现分离开来。这种类型的组织机制对那些有过程化程序设计背景的人来说,更容易理解。而多态的作用则是消除类型之间的耦合关系。在前一章中,我们已经知道继承允许将对象视为自己本身的类型或它的基类型进行处理。这种能力极为重要,因为它可以使多种类型(从同一基类导出而来的)被视为同一类型进行处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出而来的。这种区别是根据方法行为的不同来表示出来的,虽然这些方法都可以通过同一个基类来调用。在本章中,通过一些基本简单的例子(这些例子中所有与多态无关的代码都被删掉,只剩下与多态有关的部分)来深入浅出地学习多态(也称作动态绑定dynamicbinding、后期绑定latebinding或运行时绑定run-timebinding)。向上转型在第6章中,我们已经知道对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。而这种将对某个对象的引用视为对其基类型的引用的做法被称作“向上转型(upcasting)”――因为在继承树的画法中,基类是放置在上方的。但是,这样做也会引起一个的问题,具体看下面这个有关乐器的例子。既然几个例子都要演奏乐符(Note),我们就应该在包中单独创建一个Note类。//:c07:music:Note.java//Notestoplayonmusicalinstruments.packagec07.music;importcom.bruceeckel.simpletest.*;publicclassNote{privateStringnoteName;privateNote(StringnoteName){this.noteName=noteName;}publicStringtoString(){returnnoteName;}publicstaticfinalNoteMIDDLE_C=newNote(MiddleC),C_SHARP=newNote(CSharp),B_FLAT=newNote(BFlat);//Etc.}///:~这是一个枚举(enumeration)类,包含固定数目的可供选择的不变对象。不能再产生另外的对象,因为其构造器是私有的。在下面的例子中,Wind是一种Instrument,因此可以继承Instrument类。//:c07:music:Wind.javapackagec07.music;//Windobjectsareinstruments//becausetheyhavethesameinterface:publicclassWindextendsInstrument{//Redefineinterfacemethod:publicvoidplay(Noten){System.out.println(Wind.play()+n);}}///:~//:c07:music:Music.java//Inheritance&upcasting.packagec07.music;importcom.bruceeckel.simpletest.*;publicclassMusic{privatestaticTestmonitor=newTest();publicstaticvoidtune(Instrumenti){//...i.play(Note.MIDDLE_C);}publicstaticvoidmain(String[]args){Windflute=newWind();tune(flute);//Upcastingmonitor.expect(newString[]{Wind.play()MiddleC});}}///:~Music.tune()方法接受一个Instrument引用参数,同时也接受任何导出自Instrument的类。在Main()方法中,当一个Wind引用传递到tune()方法时,就会出现这种情况,而不需要任何类型转换。这样做是允许的――因为Wind从Instrument继承而来,所以Instrument的接口必定存在于Wind中。从Wind向上转型到Instrument可能会“缩小”接口,但无论如何也不会比Instrument的全部接口更窄。忘记对象类型Music.java这个程序看起来似乎有些奇怪。为什么所有人都应该故意忘记一个对象的类型呢?在进行向上转型时,就会产生这种情况;并且如果让tune()方法直接接受一个Wind引用作为自己的参数,似乎会更为直观。但这样会引发的一个重要问题是:如果你那样做,就需要为系统内Instrument的每种类型都编写一个新的tune()方法。假设按照这种推理,现在再加入Stringed(弦乐)和Brass(管乐)这两种Instrument(乐器)://:c07:music:Music2.java//Overloadinginsteadofupcasting.packagec07.music;importcom.bruceeckel.simpletest.*;classStringedextendsInstrument{publicvoidplay(Noten){System.out.println(Stringed.play()+n);}}classBrassextendsInstrument{publicvoidplay(Noten){System.out.println(Brass.play()+n);}}publicclassMusic2{privatestaticTestmonitor=newTest();publicstaticvoidtune(Windi){i.play(Note.MIDDLE_C);}publicstaticvoidtune(Stringedi){i.play(Note.MIDDLE_C);}publicstaticvoidtune(Brassi){i.play(Note.MIDDLE_C);}publicstaticvoidmain(String[]args){Windflute=newWind();Stringedviolin=newStringed();BrassfrenchHorn=newBrass();tune(flute);//Noupcastingtune(violin);tune(frenchHorn);monitor.expect(newString[]{Wind.play()MiddleC,Stringed.play()MiddleC,Brass.play()MiddleC});}}///:~这样做行得通,但有一个主要缺点:必须为添加的每一个新Instrument类编写特定类型的方法。这意味着在开始时就需要更多的编程,这也意味着如果以后想添加类似Tune()的新方法,或者添加自Instrument导出的新类,仍需要做大量的工作。此外,如果我们忘记重载某个方法,编译器不会返回任何错误信息,这样关于类型的整个处理过程就变得难以操纵。如果我们只写这样一个简单方法,它仅接收基类作为参数,而不是那些特殊的导出类。这样做情况会变得更好吗?也就是说,如果我们不管导出类的存在,编写的代码只是与基类打交道,会不会好呢?这正是多态所允许的。然而,大多数程序员具有面向过程程序设计的背景,对多态的运作方式可能会感到有一点迷惑。曲解运行Music.java这个程序后,我们便会发现难点所在。Wind.play()方法将产生输出结果。这无疑是我们所期望的输出结果,但它看起来似乎又没有什么意义。请观察一下tune()方法:publicstaticvoidtune(Instrumenti){//...i.play(Note.MIDDLE_C);}它接受一个Instrument引用。那么在这种情况下,编译器怎样才可能知道这个Instrument引用指向的是Wind对象,而不是Brass对象或Stringed对象呢?实际上,编译器无法得知。为了深入理解这个问题,有必要研究一下“绑定(binding)”这个话题。方法调用绑定将一个方法调用同一个方法主体关联起来被称作“绑定(binding)”。若在程序执行前进行绑定(如果有的话,由编译器和链接程序实现),叫做“前期绑定(earlybinding)”。可能你以前从来没有听说过这个术语,因为它是面向过程的语言中不需要选择就默认的绑定方式。C编译器只有一种方法调用,那就是前期绑定。上述程序之所以令人迷惑,主要是因为提前绑定。因为,当编译器只有一个Instrument引用时,它无法知道究竟调用哪个方法才对。解决的办法叫做“后期绑定(latebinding)”,它的含义就是在运行时,根据对象的类型进行绑定。后期绑定也叫做“动态绑定(dynamicbinding)”或“运行时绑定(run-timebinding)”。如果一种语言想实现后期绑定,就必须具有某些机制,以便在运行时能判断对象的类型,以调用恰当的方法。也就是说,编译器仍不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是我们只要想象一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。Java中除了static和final方法(private方法属于final)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定---它会自动发生。为什么要将某个方法声明为final呢?正如前一章提到的那样,它可以防止其他人重载该方法。但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者是想告诉编译器不需要对其进行动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。然而,大多数情况下,这样做对我们程序的整体性能不会产生什么改观。所以,最好根据设计来决定是否使用final,而不是出于试图提高性能。产生正确的行为一旦知道Java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。或者换种说法,发送消息给某个对象,让该对象去断定应该做什么事。面向对象程序设计中,有一个最经典的“几何形状(shape)”例子。因为它很容易被可视化,所以经常用到;但不幸的是,它可能使初学者认为面向对象程序设计仅适用于图形化程序设计,实际当然不是这种情形了。在“几何形状”这个例子中,包含一个Shape基类和多个导出类,如:Circle,Square,Triangle等。这个例子之所以好用,是因为我们可以说“圆是一种形状”,这种说法也很容易被理解。下面的继承图展示了它们之间的关系:向上转型可以像下面这条语句这么简单:Shapes=newCircle();这里,创建了一个Circle对象,并把得到的引用立即赋值给Shape,这样做看似错误(将一种类型赋值给另一类型);但实际上是没问题的,因为通过继承,Circle就是一种Shape。因此,编译器认可这条语句,也就不会产生错误信息。假设我们调用某个基类方法(已被导出类所重载):s.draw();同样地,我们可能会认为调用的是shape的draw(),因为这毕竟是一个shape引用,那么编译器是怎样知道去做其他的事情呢?由于后期绑定(多态),程序还是正确调用了Circle.draw()方法。下面的例子稍微有所不同://:c07:Shapes.java//PolymorphisminJava.
本文标题:第七章多态
链接地址:https://www.777doc.com/doc-2182311 .html