资源描述:
《list08》由会员上传分享,免费在线阅读,更多相关内容在学术论文-天天文库。
第8章继承157第8章继承继承是面向对象编程技术的一块基石,因为它允许创建分等级层次的类。运用继承,你能够创建一个通用类,它定义了一系列相关项目的一般特性。该类可以被更具体的类继承,每个具体的类都增加一些自己特有的东西。在Java术语学中,被继承的类叫超类(superclass),继承超类的类叫子类(subclass)。因此,子类是超类的一个专门用途的版本,它继承了超类定义的所有实例变量和方法,并且为它自己增添了独特的元素。8.1继承的基础继承一个类,只要用extends关键字把一个类的定义合并到另一个中就可以了。为了理解怎样继承,让我们从简短的程序开始。下面的例子创建了一个超类A和一个名为B的子类。注意怎样用关键字extends来创建A的一个子类。//Asimpleexampleofinheritance.//Createasuperclass.classA{inti,j;voidshowij(){System.out.println("iandj:"+i+""+j);}}//CreateasubclassbyextendingclassA.classBextendsA{intk;voidshowk(){System.out.println("k:"+k);}voidsum(){System.out.println("i+j+k:"+(i+j+k));}}classSimpleInheritance{publicstaticvoidmain(Stringargs[]){AsuperOb=newA();BsubOb=newB();//Thesuperclassmaybeusedbyitself.superOb.i=10;superOb.j=20;
1第8章继承157System.out.println("ContentsofsuperOb:");superOb.showij();System.out.println();/*Thesubclasshasaccesstoallpublicmembersofitssuperclass.*/subOb.i=7;subOb.j=8;subOb.k=9;System.out.println("ContentsofsubOb:");subOb.showij();subOb.showk();System.out.println();System.out.println("Sumofi,jandkinsubOb:");subOb.sum();}}该程序的输出如下:ContentsofsuperOb:iandj:1020ContentsofsubOb:iandj:78k:9Sumofi,jandkinsubOb:i+j+k:24像你所看到的,子类B包括它的超类A中的所有成员。这是为什么subOb可以获取i和j以及调用showij( )方法的原因。同样,sum( )内部,i和j可以被直接引用,就像它们是B的一部分。尽管A是B的超类,它也是一个完全独立的类。作为一个子类的超类并不意味着超类不能被自己使用。而且,一个子类可以是另一个类的超类。声明一个继承超类的类的通常形式如下:classsubclass-nameextendssuperclass-name{//bodyofclass}你只能给你所创建的每个子类定义一个超类。Java不支持多超类的继承(这与C++不同,在C++中,你可以继承多个基础类)。你可以按照规定创建一个继承的层次。该层次中,一个子类成为另一个子类的超类。然而,没有类可以成为它自己的超类。8.1.1成员的访问和继承尽管子类包括超类的所有成员,它不能访问超类中被声明成private的成员。例如,考虑下面简单的类层次结构:
2第8章继承157/*Inaclasshierarchy,privatemembersremainprivatetotheirclass.Thisprogramcontainsanerrorandwillnotcompile.*///Createasuperclass.classA{inti;//publicbydefaultprivateintj;//privatetoAvoidsetij(intx,inty){i=x;j=y;}}//A'sjisnotaccessiblehere.classBextendsA{inttotal;voidsum(){total=i+j;//ERROR,jisnotaccessiblehere}}classAccess{publicstaticvoidmain(Stringargs[]){BsubOb=newB();subOb.setij(10,12);subOb.sum();System.out.println("Totalis"+subOb.total);}}该程序不会编译,因为B中sum( )方法内部对j的引用是不合法的。既然j被声明成private,它只能被它自己类中的其他成员访问。子类没权访问它。注意:一个被定义成private的类成员为此类私有,它不能被该类外的所有代码访问,包括子类。8.1.2更实际的例子让我们看一个更实际的例子,该例子有助于阐述继承的作用。这里,前面章节改进的Box类的最后版本将被扩展。它包括第四成员名为weight
3第8章继承157。这样,新的类将包含一个盒子的宽度、高度、深度和重量。//ThisprogramusesinheritancetoextendBox.classBox{doublewidth;doubleheight;doubledepth;//constructcloneofanobjectBox(Boxob){//passobjecttoconstructorwidth=ob.width;height=ob.height;depth=ob.depth;}//constructorusedwhenalldimensionsspecifiedBox(doublew,doubleh,doubled){width=w;height=h;depth=d;}//constructorusedwhennodimensionsspecifiedBox(){width=-1;//use-1toindicateheight=-1;//anuninitializeddepth=-1;//box}//constructorusedwhencubeiscreatedBox(doublelen){width=height=depth=len;}//computeandreturnvolumedoublevolume(){returnwidth*height*depth;}}//Here,Boxisextendedtoincludeweight.classBoxWeightextendsBox{doubleweight;//weightofbox//constructorforBoxWeightBoxWeight(doublew,doubleh,doubled,doublem){width=w;height=h;depth=d;weight=m;}}classDemoBoxWeight{publicstaticvoidmain(Stringargs[]){BoxWeightmybox1=newBoxWeight(10,20,15,34.3);
4第8章继承157BoxWeightmybox2=newBoxWeight(2,3,4,0.076);doublevol;vol=mybox1.volume();System.out.println("Volumeofmybox1is"+vol);System.out.println("Weightofmybox1is"+mybox1.weight);System.out.println();vol=mybox2.volume();System.out.println("Volumeofmybox2is"+vol);System.out.println("Weightofmybox2is"+mybox2.weight);}}该程序的输出显示如下:Volumeofmybox1is3000.0Weightofmybox1is34.3Volumeofmybox2is24.0Weightofmybox2is0.076BoxWeight继承了Box的所有特征并为自己增添了一个weight成员。没有必要让BoxWeight重新创建Box中的所有特征。为满足需要我们只要扩展Box就可以了。继承的一个主要优势在于一旦你已经创建了一个超类,而该超类定义了适用于一组对象的属性,它可用来创建任何数量的说明更多细节的子类。每一个子类能够正好制作它自己的分类。例如,下面的类继承了Box并增加了一个颜色属性://Here,Boxisextendedtoincludecolor.classColorBoxextendsBox{intcolor;//colorofboxColorBox(doublew,doubleh,doubled,intc){width=w;height=h;depth=d;color=c;}}记住,一旦你已经创建了一个定义了对象一般属性的超类,该超类可以被继承以生成特殊用途的类。每一个子类只增添它自己独特的属性。这是继承的本质。8.1.3超类变量可以引用子类对象超类的一个引用变量可以被任何从该超类派生的子类的引用赋值。你将发现继承的这个方面在很多条件下是很有用的。例如,考虑下面的程序:classRefDemo{
5第8章继承157publicstaticvoidmain(Stringargs[]){BoxWeightweightbox=newBoxWeight(3,5,7,8.37);Boxplainbox=newBox();doublevol;vol=weightbox.volume();System.out.println("Volumeofweightboxis"+vol);System.out.println("Weightofweightboxis"+weightbox.weight);System.out.println();//assignBoxWeightreferencetoBoxreferenceplainbox=weightbox;vol=plainbox.volume();//OK,volume()definedinBoxSystem.out.println("Volumeofplainboxis"+vol);/*Thefollowingstatementisinvalidbecauseplainboxdoesnotdefineaweightmember.*///System.out.println("Weightofplainboxis"+plainbox.weight);}}这里,weightbox是BoxWeight对象的一个引用,plainbox是Box对象的一个引用。既然BoxWeight是Box的一个子类,允许用一个weightbox对象的引用给plainbox赋值。理解是引用变量的类型——而不是引用对象的类型——决定了什么成员可以被访问。也就是说,当一个子类对象的引用被赋给一个超类引用变量时,你只能访问超类定义的对象的那一部分。这是为什么plainbox不能访问weight的原因,甚至是它引用了一个BoxWeight对象也不行。仔细想一想,这是有道理的,因为超类不知道子类增加的属性。这就是本程序中的最后一行被注释掉的原因。Box的引用访问weight域是不可能的,因为它没有定义。尽管前面部分看起来有一点深奥,它是很重要的实际应用——本章后面将讨论的两种应用之一。8.2使用super在前面的例子中,从Box派生的类并没有体现出它们的实际上是多么有效和强大。例如,BoxWeight构造函数明确的初始化了Box( )的width、height和depth成员。这些重复的代码在它的超类中已经存在,这样做效率很低,而且,这意味着子类必须被同意具有访问这些成员的权力。然而,有时你希望创建一个超类,该超类可以保持它自己实现的细节(也就是说,它保持私有的数据成员)。这种情况下,子类没有办法直接访问或初始化它自己的这些变量。既然封装是面向对象的基本属性,Java提供了该问题的解决方案是不值得奇怪的。任何时候一个子类需要引用它直接的超类,它可以用关键字super来实现。super有两种通用形式。第一种调用超类的构造函数。第二种用来访问被子类的成员隐藏的超类成员。下面分别介绍每一种用法。
6第8章继承1578.2.1使用super调用超类构造函数子类可以调用超类中定义的构造函数方法,用super的下面形式:super(parameter-list);这里,parameter-list定义了超类中构造函数所用到的所有参数。super( )必须是在子类构造函数中的第一个执行语句。为了了解怎样运用super( ),考虑下面BoxWeight( )的改进版本://BoxWeightnowusessupertoinitializeitsBoxattributes.classBoxWeightextendsBox{doubleweight;//weightofbox//initializewidth,height,anddepthusingsuper()BoxWeight(doublew,doubleh,doubled,doublem){super(w,h,d);//callsuperclassconstructorweight=m;}}这里,BoxWeight( )调用带w、h和d参数的super( )方法。这使Box( )构造函数被调用,用w、h和d来初始化width,height,和depth。BoxWeight不再自己初始化这些值。它只需初始化它自己的特殊值:weight。这种方法使Box可以自由的根据需要把这些值声明成private。上面的例子,调用super( )用了三个参数。既然构造函数可以被重载,可以用超类定义的任何形式调用super( ),执行的构造函数将是与所传参数相匹配的那一个。例如,下面是BoxWeight一个完整的实现,BoxWeight具有以不同方法构造盒子的构造函数。在每种情况下,用适当的参数调用super( )。注意width,height,anddepth在Box是私有的。//AcompleteimplementationofBoxWeight.classBox{privatedoublewidth;privatedoubleheight;privatedoubledepth;//constructcloneofanobjectBox(Boxob){//passobjecttoconstructorwidth=ob.width;height=ob.height;depth=ob.depth;}//constructorusedwhenalldimensionsspecifiedBox(doublew,doubleh,doubled){width=w;height=h;depth=d;}
7第8章继承157//constructorusedwhennodimensionsspecifiedBox(){width=-1;//use-1toindicateheight=-1;//anuninitializeddepth=-1;//box}//constructorusedwhencubeiscreatedBox(doublelen){width=height=depth=len;}//computeandreturnvolumedoublevolume(){returnwidth*height*depth;}}//BoxWeightnowfullyimplementsallconstructors.classBoxWeightextendsBox{doubleweight;//weightofbox//constructcloneofanobjectBoxWeight(BoxWeightob){//passobjecttoconstructorsuper(ob);weight=ob.weight;}//constructorwhenallparametersarespecifiedBoxWeight(doublew,doubleh,doubled,doublem){super(w,h,d);//callsuperclassconstructorweight=m;}//defaultconstructorBoxWeight(){super();weight=-1;}//constructorusedwhencubeiscreatedBoxWeight(doublelen,doublem){super(len);weight=m;}}classDemoSuper{publicstaticvoidmain(Stringargs[]){BoxWeightmybox1=newBoxWeight(10,20,15,34.3);BoxWeightmybox2=newBoxWeight(2,3,4,0.076);BoxWeightmybox3=newBoxWeight();//defaultBoxWeightmycube=newBoxWeight(3,2);BoxWeightmyclone=newBoxWeight(mybox1);doublevol;
8第8章继承157vol=mybox1.volume();System.out.println("Volumeofmybox1is"+vol);System.out.println("Weightofmybox1is"+mybox1.weight);System.out.println();vol=mybox2.volume();System.out.println("Volumeofmybox2is"+vol);System.out.println("Weightofmybox2is"+mybox2.weight);System.out.println();vol=mybox3.volume();System.out.println("Volumeofmybox3is"+vol);System.out.println("Weightofmybox3is"+mybox3.weight);System.out.println();vol=myclone.volume();System.out.println("Volumeofmycloneis"+vol);System.out.println("Weightofmycloneis"+myclone.weight);System.out.println();vol=mycube.volume();System.out.println("Volumeofmycubeis"+vol);System.out.println("Weightofmycubeis"+mycube.weight);System.out.println();}}该程序产生下面的输出:Volumeofmybox1is3000.0Weightofmybox1is34.3Volumeofmybox2is24.0Weightofmybox2is0.076Volumeofmybox3is-1.0Weightofmybox3is-1.0Volumeofmycloneis3000.0Weightofmycloneis34.3Volumeofmycubeis27.0Weightofmycubeis2.0特别注意BoxWeight( )中的这个构造函数://constructcloneofanobjectBoxWeight(BoxWeightob){//passobjecttoconstructorsuper(ob);weight=ob.weight;}注意super( )被用一个BoxWeight类型而不是Box类型的对象调用。这仍然调用了构造函数Box(Boxob)。前面已经提醒过,一个超类变量可以引用作为任何一个从它派生的对象。因此,我们可以传递一个BoxWeight对象给Box构造函数。当然,Box
9第8章继承157只知道它自己成员的信息。让我们复习super( )中的关键概念。当一个子类调用super( ),它调用它的直接超类的构造函数。这样,super( )总是引用调用类直接的超类。这甚至在多层次结构中也是成立的。还有,super( )必须是子类构造函数中的第一个执行语句。8.2.2Super的第2种用法Super的第2种形式,除了总是引用它所在子类的超类,它的行为有点像this。这种用法有下面的通用形式:super.member这里,member既可以是1个方法也可以是1个实例变量。Super的第2种形式多数是用于超类成员名被子类中同样的成员名隐藏的情况。思考下面简单的层次://Usingsupertoovercomenamehiding.classA{inti;}//CreateasubclassbyextendingclassA.classBextendsA{inti;//thisihidestheiinAB(inta,intb){super.i=a;//iinAi=b;//iinB}voidshow(){System.out.println("iinsuperclass:"+super.i);System.out.println("iinsubclass:"+i);}}classUseSuper{publicstaticvoidmain(Stringargs[]){BsubOb=newB(1,2);subOb.show();}}该程序输出如下:iinsuperclass:1iinsubclass:2尽管B中的实例变量i隐藏了A中的i,使用super就可以访问超类中定义的i。你将会看到,super也可以用来调用超类中被子类隐藏的方法。
10第8章继承1578.3创建多级类层次到目前为止,我们已经用到了只含有一个超类和一个子类的简单类层次结构。然而,你可以如你所愿的建立包含任意多层继承的类层次。前面提到,用一个子类作为另一个类的超类是完全可以接受的。例如,给定三个类A,B和C。C是B的一个子类,而B又是A的一个子类。当这种类型的情形发生时,每个子类继承它的所有超类的属性。这种情况下,C继承B和A的所有方面。为了理解多级层次的用途,考虑下面的程序。该程序中,子类BoxWeight用作超类来创建一个名为Shipment的子类。Shipment继承了BoxWeight和Box的所有特征,并且增加了一个名为cost的成员,该成员记录了运送这样一个小包的费用。//ExtendBoxWeighttoincludeshippingcosts.//StartwithBox.classBox{privatedoublewidth;privatedoubleheight;privatedoubledepth;//constructcloneofanobjectBox(Boxob){//passobjecttoconstructorwidth=ob.width;height=ob.height;depth=ob.depth;}//constructorusedwhenalldimensionsspecifiedBox(doublew,doubleh,doubled){width=w;height=h;depth=d;}//constructorusedwhennodimensionsspecifiedBox(){width=-1;//use-1toindicateheight=-1;//anuninitializeddepth=-1;//box}//constructorusedwhencubeiscreatedBox(doublelen){width=height=depth=len;}//computeandreturnvolumedoublevolume(){returnwidth*height*depth;
11第8章继承157}}//Addweight.classBoxWeightextendsBox{doubleweight;//weightofbox//constructcloneofanobjectBoxWeight(BoxWeightob){//passobjecttoconstructorsuper(ob);weight=ob.weight;}//constructorwhenallparametersarespecifiedBoxWeight(doublew,doubleh,doubled,doublem){super(w,h,d);//callsuperclassconstructorweight=m;}//defaultconstructorBoxWeight(){super();weight=-1;}//constructorusedwhencubeiscreatedBoxWeight(doublelen,doublem){super(len);weight=m;}}//AddshippingcostsclassShipmentextendsBoxWeight{doublecost;//constructcloneofanobjectShipment(Shipmentob){//passobjecttoconstructorsuper(ob);cost=ob.cost;}//constructorwhenallparametersarespecifiedShipment(doublew,doubleh,doubled,doublem,doublec){super(w,h,d,m);//callsuperclassconstructorcost=c;}//defaultconstructorShipment(){super();cost=-1;}//constructorusedwhencubeiscreatedShipment(doublelen,doublem,doublec){super(len,m);
12第8章继承157cost=c;}}classDemoShipment{publicstaticvoidmain(Stringargs[]){Shipmentshipment1=newShipment(10,20,15,10,3.41);Shipmentshipment2=newShipment(2,3,4,0.76,1.28);doublevol;vol=shipment1.volume();System.out.println("Volumeofshipment1is"+vol);System.out.println("Weightofshipment1is"+shipment1.weight);System.out.println("Shippingcost:$"+shipment1.cost);System.out.println();vol=shipment2.volume();System.out.println("Volumeofshipment2is"+vol);System.out.println("Weightofshipment2is"+shipment2.weight);System.out.println("Shippingcost:$"+shipment2.cost);}}下面是该程序的输出:Volumeofshipment1is3000.0Weightofshipment1is10.0Shippingcost:$3.41Volumeofshipment2is24.0Weightofshipment2is0.76Shippingcost:$1.28因为继承关系,Shipment可以利用原先定义好的Box和BoxWeight类,仅为自己增加特殊用途的其他信息。这体现了继承的部分价值;它允许代码重用。该例阐述了另一个重要的知识点:super( )总是引用子类最接近的超类的构造函数。Shipment中super( )调用了BoxWeight的构造函数。BoxWeight中的super( )调用了Box中的构造函数。在类层次结构中,如果超类构造函数需要参数,那么不论子类它自己需不需要参数,所有子类必须向上传递这些参数。注意:在前面的例子中,整个类层次,包括Box,BoxWeight和Shipment
13第8章继承157,在一个文件中显示。这仅仅根据简便程度而定。Java中所有三个类可以被放置在它们自己的文件中且可以独立编译。实际上,在创建类层次结构的时候,使用分离的文件是常见的,不是罕见的。8.4何时调用构造函数类层次结构创建以后,组成层次结构的类的构造函数以怎样的顺序被调用?举个例子来说,给定一个名为B的子类和超类A,是A的构造函数在B的构造函数之前调用,还是情况相反?回答是在类层次结构中,构造函数以派生的次序调用,从超类到子类。而且,尽管super( )必须是子类构造函数的第一个执行语句,无论你用到了super( )没有,这个次序不变。如果super( )没有被用到,每个超类的默认的或无参数的构造函数将执行。下面的例子阐述了何时执行构造函数://Demonstratewhenconstructorsarecalled.//Createasuperclass.classA{A(){System.out.println("InsideA'sconstructor.");}}//CreateasubclassbyextendingclassA.classBextendsA{B(){System.out.println("InsideB'sconstructor.");}}//CreateanothersubclassbyextendingB.classCextendsB{C(){System.out.println("InsideC'sconstructor.");}}classCallingCons{publicstaticvoidmain(Stringargs[]){Cc=newC();}}该程序输出如下:InsideA’sconstructorInsideB’sconstructorInsideC’sconstructor
14第8章继承157如你所见,构造函数以派生的顺序被调用。仔细考虑,构造函数以派生的顺序执行是很有意义的。因为超类不知道任何子类的信息,任何它需要完成的初始化是与子类的初始化分离的,而且它可能是完成子类初始化的先决条件。因此,它必须最先执行。8.5方法重载类层次结构中,如果子类中的一个方法与它超类中的方法有相同的方法名和类型声明,称子类中的方法重载(override)超类中的方法。从子类中调用重载方法时,它总是引用子类定义的方法。而超类中定义的方法将被隐藏。考虑下面程序://Methodoverriding.classA{inti,j;A(inta,intb){i=a;j=b;}//displayiandjvoidshow(){System.out.println("iandj:"+i+""+j);}}classBextendsA{intk;B(inta,intb,intc){super(a,b);k=c;}//displayk–thisoverridesshow()inAvoidshow(){System.out.println("k:"+k);}}classOverride{publicstaticvoidmain(Stringargs[]){BsubOb=newB(1,2,3);subOb.show();//thiscallsshow()inB}}
15第8章继承157程序输出如下:k:3当一个B类的对象调用show( )时,调用的是在B中定义的show( )版本。也就是说,B中的show( )方法重载了A中声明的show( )方法。如果你希望访问被重载的超类的方法,可以用super。例如,在下面的B的版本中,在子类中超类的show( )方法被调用。这使所有的实例变量被显示。classBextendsA{intk;B(inta,intb,intc){super(a,b);k=c;}voidshow(){super.show();//thiscallsA'sshow()System.out.println("k:"+k);}}如果你用该版本的A代替先前的版本形式,将会得出下面输出:iandj:12k:3这里,super.show( )调用了超类的show( )方法。方法覆盖仅在两个方法的名称和类型声明都相同时才发生。如果它们不同,那么两个方法就只是重载。例如,考虑下面的程序,它修改了前面的例子://Methodswithdifferingtypesignaturesareoverloaded–not//overridden.classA{inti,j;A(inta,intb){i=a;j=b;}//displayiandjvoidshow(){System.out.println("iandj:"+i+""+j);}}//CreateasubclassbyextendingclassA.classBextendsA{intk;B(inta,intb,intc){super(a,b);
16第8章继承157k=c;}//overloadshow()voidshow(Stringmsg){System.out.println(msg+k);}}classOverride{publicstaticvoidmain(Stringargs[]){BsubOb=newB(1,2,3);subOb.show("Thisisk:");//thiscallsshow()inBsubOb.show();//thiscallsshow()inA}}该程序的输出显示如下:Thisisk:3iandj:12B中show( )带有一个字符串参数。这是它的类型标签与A中的不同,A中的show( )没有带参数。因此没有覆盖(或名称隐藏)发生。8.6动态方法调度前面的例题说明了方法重载机制,但并没有显示它们的作用。实际上,如果方法重载只是一个名字空间的约定,那么它最多是有趣的,但是没有实际价值的。然而,情况并不如此。方法重载构成Java的一个最强大的概念的基础:动态方法调度(dynamicmethoddispatch)。动态方法调度是一种在运行时而不是编译时调用重载方法的机制。动态方法调度是很重要的,因为这也是Java实现运行时多态性的基础。让我们从重述一个重要的原则开始:超类的引用变量可以引用子类对象。Java用这一事实来解决在运行期间对重载方法的调用。过程如下:当一个重载方法通过超类引用被调用,Java根据当前被引用对象的类型来决定执行哪个版本的方法。如果引用的对象类型不同,就会调用一个重载方法的不同版本。换句话说,是被引用对象的类型(而不是引用变量的类型)决定执行哪个版本的重载方法。因此,如果超类包含一个被子类重载的方法,那么当通过超类引用变量引用不同对象类型时,就会执行该方法的不同版本。下面是阐述动态方法调度的例子://DynamicMethodDispatchclassA{voidcallme(){System.out.println("InsideA'scallmemethod");
17第8章继承157}}classBextendsA{//overridecallme()voidcallme(){System.out.println("InsideB'scallmemethod");}}classCextendsA{//overridecallme()voidcallme(){System.out.println("InsideC'scallmemethod");}}classDispatch{publicstaticvoidmain(Stringargs[]){Aa=newA();//objectoftypeABb=newB();//objectoftypeBCc=newC();//objectoftypeCAr;//obtainareferenceoftypeAr=a;//rreferstoanAobjectr.callme();//callsA'sversionofcallmer=b;//rreferstoaBobjectr.callme();//callsB'sversionofcallmer=c;//rreferstoaCobjectr.callme();//callsC'sversionofcallme}}该程序的输出如下:InsideA’scallmemethodInsideB’scallmemethodInsideC’scallmemethod程序创建了一个名为A的超类以及它的两个子类B和C。子类B和C重载A中定义的callme( )方法。main( )主函数中,声明了A、B和C类的对象。而且,一个A类型的引用r也被声明。就像输出所显示的,所执行的callme( )版本由调用时引用对象的类型决定。如果它是由引用变量r的类型决定的,你将会看到对A的callme( )方法的三次调用。熟悉C++的读者会认同Java中的重载方法与C++中的虚函数类似。8.6.1为什么要重载方法前面声明过,重载方法允许Java支持运行时多态性。多态性是面向对象编程的本质,原因如下:它允许通用类指定方法,这些方法对该类的所有派生类都是公用的。同时该方法允许子类定义这些方法中的某些或全部的特殊实现。重载方法是Java实现它的多态性——“一个接口,多个方法”的另一种方式。
18第8章继承157成功应用多态的关键部分是理解超类和子类形成了一个从简单到复杂类层次。正确应用多态,超类提供子类可以直接运用的所有元素。多态也定义了这些派生类必须自己实现的方法。这允许子类在加强一致接口的同时,灵活的定义它们自己的方法。这样,通过继承和重载方法的联合,超类可以定义供它的所有子类使用的方法的通用形式。动态的运行时多态是面向对象设计代码重用的一个最强大的机制。现有代码库在维持抽象接口同时不重新编译的情况下调用新类实例的能力是一个极其强大的工具。8.6.2应用方法重载让我们看一个运用方法重载的更实际的例子。下面的程序创建了一个名为Figure的超类,它存储不同二维对象的大小。它还定义了一个方法area( ),该方法计算对象的面积。程序从Figure派生了两个子类。第一个是Rectangle,第二个是Triangle。每个子类重载area( )方法,它们分别返回一个矩形和一个三角形的面积。//Usingrun-timepolymorphism.classFigure{doubledim1;doubledim2;Figure(doublea,doubleb){dim1=a;dim2=b;}doublearea(){System.out.println("AreaforFigureisundefined.");return0;}}classRectangleextendsFigure{Rectangle(doublea,doubleb){super(a,b);}//overrideareaforrectangledoublearea(){System.out.println("InsideAreaforRectangle.");returndim1*dim2;}}classTriangleextendsFigure{Triangle(doublea,doubleb){super(a,b);}//overrideareaforrighttriangle
19第8章继承157doublearea(){System.out.println("InsideAreaforTriangle.");returndim1*dim2/2;}}classFindAreas{publicstaticvoidmain(Stringargs[]){Figuref=newFigure(10,10);Rectangler=newRectangle(9,5);Trianglet=newTriangle(10,8);Figurefigref;figref=r;System.out.println("Areais"+figref.area());figref=t;System.out.println("Areais"+figref.area());figref=f;System.out.println("Areais"+figref.area());}}该程序输出如下:InsideAreaforRectangle.Areais45InsideAreaforTriangle.Areais40AreaforFigureisundefined.Areais0通过继承和运行时多态的双重机制,可以定义一个被很多不同却有关的对象类型运用的一致的接口。这种情况下,如果一个对象是从Figure派生,那么它的面积可以由调用area( )来获得。无论用到哪种图形的类型,该操作的接口是相同的。
20第8章继承1578.7使用抽象类有些情况下,你希望定义一个超类,该超类定义了一种给定结构的抽象但是不提供任何完整的方法实现。也就是说,有时你希望创建一个只定义一个被它的所有子类共享的通用形式,由每个子类自己去填写细节。这样的类决定了子类所必须实现的方法的本性。这类情形下一种可能发生的情况是超类不能创建一个方法的有意义的实现。前面的例子中用到的类Figure就属于这种情况。area( )的定义仅是一个占位符。它不会计算和显示任何类型对象的面积。当创建自己的类库时你会看到,超类中的方法没有实际意义并不罕见。你有两种方法可以处理这种情况。第一种,如前面的例子所示,仅仅是报告一个出错消息。尽管这种方式在某些场合是有用的——例如调试——但是它不是很适用的。你还有一种方法就是通过子类重载该方法以使它对子类有意义。考虑Triangle类,如果不定义area( )它是毫无意义的。这种情况下,你希望有方法确保子类真正重载了所有必须的方法。Java对于这个问题的解决是用抽象方法(abstractmethod)。你可以通过指定abstract类型修饰符由子类重载某些方法。这些方法有时被作为子类责任(subclasserresponsibility)引用,因为它们没有在超类中指定的实现。这样子类必须重载它们——它们不能简单地使用超类中定义的版本。声明一个抽象方法,用下面的通用形式:abstracttypename(parameter-list);正如你所看到的,不存在方法体。任何含有一个或多个抽象方法的类都必须声明成抽象类。声明一个抽象类,只需在类声明开始时在关键字class前使用关键字abstract。抽象类没有对象。也就是说,一个抽象类不能通过new操作符直接实例化。这样的对象是无用的,因为抽象类是不完全定义的。而且,你不能定义抽象构造函数或抽象静态方法。所有抽象类的子类都必须执行超类中的所有抽象方法或者是它自己也声明成abstract。下面是具有一个抽象方法类的简单例题。该类后面是一个执行抽象方法的类://ASimpledemonstrationofabstract.abstractclassA{abstractvoidcallme();//concretemethodsarestillallowedinabstractclassesvoidcallmetoo(){System.out.println("Thisisaconcretemethod.");}}classBextendsA{
21第8章继承157voidcallme(){System.out.println("B'simplementationofcallme.");}}classAbstractDemo{publicstaticvoidmain(Stringargs[]){Bb=newB();b.callme();b.callmetoo();}}注意程序中声明A的对象。刚刚讲过,实例化一个抽象类是不可能的。另外一点要注意:类A实现一个具体的方法callmetoo( )。这是完全可接受的,抽象类可以包括它们合适的很多实现。因为Java的运行时多态是通过使用超类引用实现的,所以尽管抽象类不能用来实例化,它们可以用来创建对象引用。这样,创建一个抽象类的引用是可行的,这样它可以用来指向一个子类对象。在下面的程序中你将会看到这种特性的运用。运用抽象类,你可以改善前面所显示的Figure类。因为对于一个未定义的二维图形,面积的概念是没有意义的,下面的程序在Figure内将area( )定义成抽象方法。这样当然意味着从Figure派生的所有类都必须重载area( )方法。//Usingabstractmethodsandclasses.abstractclassFigure{doubledim1;doubledim2;Figure(doublea,doubleb){dim1=a;dim2=b;}//areaisnowanabstractmethodabstractdoublearea();}classRectangleextendsFigure{Rectangle(doublea,doubleb){super(a,b);}//overrideareaforrectangledoublearea(){System.out.println("InsideAreaforRectangle.");returndim1*dim2;
22第8章继承157}}classTriangleextendsFigure{Triangle(doublea,doubleb){super(a,b);}//overrideareaforrighttriangledoublearea(){System.out.println("InsideAreaforTriangle.");returndim1*dim2/2;}}classAbstractAreas{publicstaticvoidmain(Stringargs[]){//Figuref=newFigure(10,10);//illegalnowRectangler=newRectangle(9,5);Trianglet=newTriangle(10,8);Figurefigref;//thisisOK,noobjectiscreatedfigref=r;System.out.println("Areais"+figref.area());figref=t;System.out.println("Areais"+figref.area());}}Main()内的注释暗示,定义Figure类型的对象不再是可能的了,因为现在它是抽象类。而且,所有Figure的子类都必须重载area( )方法。为证明这点,试着创建不重载area( )的子类。你会收到一个编译时错误。尽管不可能创建一个Figure类型的对象,你可以创建一个Figure类型的引用变量。变量figref声明成Figure的一个引用,意思是说它可以用来引用任何从Figure派生的对象。刚才解释过的,通过超类引用变量重载方法在运行时解决。8.8继承中使用finalFinal关键字有三个用途。第一,它可以用来创建一个已命名常量的等价物。这个用法在前面的章节中已有描述。Final
23第8章继承157的其他两个用法是应用于继承的,这两种用法都会在下面阐述。8.8.1使用final阻止重载尽管方法重载是Java的一个最强大的特性,有些时候你希望防止它的发生。不接受方法被重载,在方法前定义final修饰符。声明成final的方法不能被重载。下面的程序段阐述了final的用法:classA{finalvoidmeth(){System.out.println("Thisisafinalmethod.");}}classBextendsA{voidmeth(){//ERROR!Can'toverride.System.out.println("Illegal!");}}因为meth( )被声明成final,它不能被B重载,如果你试图这样做,将会生成一个编译时错误。定义成final的方法有时可以提高程序性能:编译器可以自由的内嵌调用final方法因为它知道这些方法不能被子类重载。当一个小的final函数被调用,通常Java编译器可以通过调用方法的编译代码直接内嵌来备份子程序的字节码,这样减小了与方法调用有关的昂贵开销。内嵌仅仅是final方法的一个可选项。通常,Java在运行时动态的调用方法,这叫做后期绑定(latebinding)。然而既然final方法不能被重载,对方法的调用可以在编译时解决,这叫做早期绑定(earlybinding)。8.8.2使用final阻止继承有时你希望防止一个类被继承。做到这点只需在类声明前加final。声明一个final类含蓄的宣告了它的所有方法也都是final。你可能会想到,声明一个既是abstract的又是final的类是不合法的,因为抽象类本身是不完整的,它依靠它的子类提供完整的实现。下面是一个final类的例子:finalclassA{//...}//Thefollowingclassisillegal.classBextendsA{//ERROR!Can'tsubclassA//...}像注释暗示的,B继承A是不合法的,因为A声明成final。
24第8章继承1578.9Object类有一种由Java定义的特殊的类Object。所有其他的类都是Object的子类。也就是说,Object是所有其他类的超类。这意味着一个Object类型的引用变量可以引用其他任何一个类的对象。同样,因为数组像类一样执行,Object类型变量可以引用任何数组。Object定义了下面的方法,意味着它们可以被用于任何对象,如表8-1所示。表8-1Object类定义的方法及其用途方法用途Objectclone( )创建一个和被复制的对象完全一样的新对象booleanequals(Objectobject)判定对象是否相等voidfinalize( )在一个不常用的对象被使用前调用ClassgetClass( )获取运行时一个对象的类inthashCode( )返回调用对象有关的散列值voidnotify( )恢复一个等待调用对象线程的执行voidnotifyAll( )恢复所有等待调用对象线程的执行StringtoString( )voidwait( )voidwait(longmilliseconds)voidwait(longmilliseconds, intnanoseconds)返回描述对象的一个字符串等待另一个线程的执行getClass( ),notify( ),notifyAll( )和wait( )方法被定义成final。你可以重载除这些方法以外的其他方法。这些方法在本书的其他地方有所描述。然而,现在注意两个方法:equals( )和toString( )。equals( )方法比较两个对象的内容。如果对象是相等的,它返回true,否则返回false。toString( )方法返回一个包含调用它的对象描述的字符串。而且,该方法在对象用println( )输出时自动调用。很多类重载该方法。这样做使它们生成它们创建对象类型的一个特殊描述。要了解更多toString( )信息请参看第13章。