第七章 多态.docx
- 文档编号:10856756
- 上传时间:2023-02-23
- 格式:DOCX
- 页数:43
- 大小:35.20KB
第七章 多态.docx
《第七章 多态.docx》由会员分享,可在线阅读,更多相关《第七章 多态.docx(43页珍藏版)》请在冰豆网上搜索。
第七章多态
第七章多态
在面向对象的程序设计语言中,多态(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;}
publicstaticfinalNote
MIDDLE_C=newNote("MiddleC"),
C_SHARP=newNote("CSharp"),
B_FLAT=newNote("BFlat");
//Etc.
}
///:
~
这是一个枚举(enumeration)类,包含固定数目的可供选择的不变对象。
不能再产生另外
的对象,因为其构造器是私有的。
在下面的例子中,Wind是一种Instrument,因此可以继承Instrument类。
//:
c07:
music:
Wind.java
packagec07.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);//Upcasting
monitor.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);//Noupcasting
tune(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-time
binding)”。
如果一种语言想实现后期绑定,就必须具有某些机制,以便在运行时能判断
对象的类型,以调用恰当的方法。
也就是说,编译器仍不知道对象的类型,但是方法调用机
制能找到正确的方法体,并加以调用。
后期绑定机制随编程语言的不同而有所不同,但是我
们只要想象一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。
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.
importcom.bruceeckel.simpletest.*;
importjava.util.*;
classShape{
voiddraw(){}
voiderase(){}
}
classCircleextendsShape{
voiddraw(){
System.out.println("Circle.draw()");
}
voiderase(){
System.out.println("Circle.erase()");
}
}
classSquareextendsShape{
voiddraw(){
System.out.println("Square.draw()");
}
voiderase(){
System.out.println("Square.erase()");
}
}
classTriangleextendsShape{
voiddraw(){
System.out.println("Triangle.draw()");
}
voiderase(){
System.out.println("Triangle.erase()");
}
}
//A"factory"thatrandomlycreatesshapes:
classRandomShapeGenerator{
privateRandomrand=newRandom();
publicShapenext(){
switch(rand.nextInt(3)){
default:
case0:
returnnewCircle();
case1:
returnnewSquare();
case2:
returnnewTriangle();
}
}
}
publicclassShapes{
privatestaticTestmonitor=newTest();
privatestaticRandomShapeGeneratorgen=
newRandomShapeGenerator();
publicstaticvoidmain(String[]args){
Shape[]s=newShape[9];
//Fillupthearraywithshapes:
for(inti=0;i s[i]=gen.next(); //Makepolymorphicmethodcalls: for(inti=0;i s[i].draw(); monitor.expect(newObject[]{ newTestExpression("%%(Circle|Square|Triangle)" +"\\.draw\\(\\)",s.length) }); } } ///: ~ Shape基类为自它那里继承而来的所有导出类,建立了一个通用接口——也就是说,所有 形状都可以描绘和擦除。 导出类重载了这些定义,以便为每种特殊类型的几何形状提供独特 的行为。 RandomShapeGenerator是一种“工厂(factory)”,在我们每次调用next()方法时,它可以为随机选择的shape对象产生一个引用。 请注意向上转型是在return语句里发生的。 每个return语句取得一个指向某个Circle、Square或者Triangle的句柄,并将其以Shape类型从next()方法中发送出去。 所以无论我们在什么时候调用next()方法时,是绝对没有可能知道它所获的具体类型到底是什么,因为我们总是只能获得一个通用的Shape引用。 main()包含了Shape句柄的一个数组,通过调用RandomShapeGenerator.next()来填入数据。 此时,我们只知道自己拥有一些Shape,不会知道除此之外的更具体情况(编译器一样不知)。 然而,当我们遍历这个数组,并为每个数组元素调用draw()方法时,与各类型有关的专属行为竟会神奇般地正确发生,我们可以从运行该程序时,产生的输出结果中发现这一点。 随机选择几何形状是为了让大家理解: 在编译期间,编译器不需要获得任何特殊的信息,就 能进行正确的调用。 对draw()方法的所有调用都是通过动态绑定进行的。 扩展性 现在,让我们返回到乐器(Instrument)示例。 由于有多态机制,我们可根据自己的需求 向系统中添加任意多的新类型,而不需更修改true()方法。 在一个设计良好的OOP程序中, 我们的大多数或者所有方法都会遵循tune()的模型,而且只与基类接口通信。 我们说这样 的程序是“可扩展的”,因为我们可以从通用的基类继承出新的数据类型,从而新添一些功能。 那些操纵基类接口的如方法不需要任何改动就可以应用于新类。 考虑一下: 对于乐器例子,如果我们向基类中添加更多的方法,并加入一些新类,将会出现 什么情况呢? 如下图所示: 事实上,不需要改动tune()方法,所有的新类都能与原有类一起正确运行。 即使tune()方法是存放在某个单独文件中,并且在Instrument接口中还添加了其他的新方法, tune()也不需再编译就仍能正确运行。 下面是上述示意图的具体实现: //: c07: music3: Music3.java //Anextensibleprogram. packagec07.music3; importcom.bruceeckel.simpletest.*; importc07.music.Note; classInstrument{ voidplay(Noten){ System.out.println("Instrument.play()"+n); } Stringwhat(){return"Instrument";} voidadjust(){} } classWindextendsInstrument{ voidplay(Noten){ System.out.println("Wind.play()"+n); } Stringwhat(){return"Wind";} voidadjust(){} } classPercussionextendsInstrument{ voidplay(Noten){ System.out.println("Percussion.play()"+n); } Stringwhat(){return"Percussion";} voidadjust(){} } classStringedextendsInstrument{ voidplay(Noten){ System.out.println("Stringed.play()"+n); } Stringwhat(){return"Stringed";} voidadjust(){} } classBrassextendsWind{ voidplay(Noten){ System.out.println("Brass.play()"+n); } voidadjust(){ System.out.println("Brass.adjust()"); } } classWoodwindextendsWind{ voidplay(Noten){ System.out.println("Woodwind.play()"+n); } Stringwhat(){return"Woodwind";} } publicclassMusic3{ privatestaticTestmonitor=newTest(); //Doesn'tcareabouttype,sonewtypes //addedtothesystemstillworkright: publicstaticvoidtune(Instrumenti){ //... i.play(Note.MIDDLE_C); } publicstaticvoidtuneAll(Instrument[]e){ for(inti=0;i tune(e[i]); } publicstaticvoidmain(String[]args){ //Upcastingduringadditiontothearray: Instrument[]orchestra={ newWind(), newPercussion(), newStringed(), newBrass(), newWoodwind() }; tuneAll(orchestra); monitor.expect(newString[]{ "Wind.play()MiddleC", "Percussion.play()MiddleC", "Stringed.play()MiddleC", "Brass.play()MiddleC
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第七章 多态 第七