第5章 继承与多态 本章讨论面向对象程序设计另外两个最重要的特点: 继承与多态。 继承是面向对象程序设计方法中实现软件重用的一种重要手段,通过继承可以更有效地组织程序结构,明确类之间的关系,并充分利用已有的类来创建新类,以完成更复杂的设计和开发。多态则可以统一多个相关类的对外接口,并在运行时根据不同的情况执行不同的操作,提高类的抽象度和灵活性。 5.1子类、父类与继承机制 5.1.1继承的概念 在面向对象技术中,继承是最为显著的一个特征。继承表示存在于面向对象程序中的两个类之间的一种关系。当一个类充当另一个类的子类,就自动拥有另一个类的所有非私有属性(域和方法)时,就称这两个类之间具有继承关系。被继承的类称为父类(或超类),继承了父类的类称为子类。 在类的定义过程中,继承是一种由已有的类创建新类的机制。子类不仅可从父类中继承域和方法,而且可重定义及扩充新的内容。一般方法是: 创建一个共有属性的父类,再创建具有特殊属性的子类。例如,将平面上的坐标点构成一个点类,则平面上的圆类可继承点类的所有属性, 图5.1“圆”类继承“点”类图示 并以继承的坐标点为圆心,再自定义一个域为半径,完成圆上的各种操作,如图5.1所示。 一个父类可以同时拥有多个子类,此时该父类实际上是所有子类的公共域和方法的集合,而每一个子类则是对公共域和方法在功能、内涵方面的扩展和延伸。 父类、子类间的关系具有以下特性。  共享性,即子类可以共享父类的公共域和方法。  差异性,即子类和父类一定会存在某些差异,否则就应该是同一个类。  层次性,即由Java规定的单继承性,每个类都处于继承关系中的某一个层面。 以电话为例,图5.2列举了各种电话类的层次结构以及这些类的域和方法。 从图5.2中可以看出,面向对象的程序设计中这种继承关系符合人们通常的思维方式。它描述了一种分类关系: 电话分为固定电话和移动电话两大类,固定电话可分为IP电话和普通电话等。其中,抽象的电话类是所有电话类的父类,它包含所有电话的公共属性。这些公共属性包括话费余额等数据属性,以及计费方式、查询余额等行为属性。将电话分类并具体化,就分别派生出两个子类: 固定电话和移动电话。这两个子类一方面继承了父类电话的所有属性,即也拥有话费余额、计费方式、查询余额等属性,另一方面又专门定义了适合本身特殊需要的属性,如对于固定电话, 图5.2电话及其子类的继承关系 应该有座机费属性,这些属性对移动电话不适合(这里设移动电话没有座机费)。从固定电话到IP电话和普通电话的继承遵循相同的原则。 采用面向对象的设计方法,可以仅在抽象的父类——电话类中定义电话号码、话费余额等公共属性,其多个子类则从父类那里继承这些属性。当公共属性发生修改时,只需要在父类中修改一次即可,这不但维护工作量可以大大减少,而且可以避免修改遗漏引起的不一致性。 继承的主要优点如下。  程序结构清晰。  编程量减少。  易于修改和维护。 一般面向对象程序设计语言中的继承分为: 单继承和多继承。单继承采用树状结构,设计、实现容易; 而多继承采用网状结构,设计、实现较复杂。 Java语言出于安全性和可靠性的考虑,仅提供了单继承机制,即Java程序中的每个类只允许有一个直接的父类,而Java多继承的功能则是通过接口机制来实现的。 5.1.2类的层次 Java语言中类的单继承机制使得Java的类具有严格的层次结构。除Object类之外,每个类都有唯一的父类。这种性质使得类的层次结构形成了如图5.3所示的一种树状结构。Object类定义和实现了Java系统所需要的众多类的共同行为,它是所有类的父类,也即这个树状结构中的根类,所有的类都是由这个类继承、扩充而来的。这个Object类定义在java.lang包中。 图5.3Java语言中类的层次 5.2继承的实现 本节要讨论Java如何实现其继承性以及继承机制中的一些细节问题。 5.2.1子类的定义 定义一个子类,即在定义一个类的时候加上extends关键字,并在之后带上其父类名,其一般格式为: [类的修饰符] class<子类名> extends <父类名>{ <域定义>; <方法定义>; } 这和前面定义类的格式并没有什么区别,只是“extends <父类名>”不再是可选项。 【例5.1】图5.1指出圆类继承点类,即先定义一个点类Point,然后由点类派生一个圆类Circle。其实现代码为: class Point { int x, y; void setXY(int i, int j) { x=i; y=j; } } class Circle extends Point { double r; double area(){ return 3.14*r*r; } } 在定义子类时用extends关键字指明新定义类的父类,就在两个类之间建立了继承关系。新定义的子类可以从父类那里继承所有非private的域和方法作为自己的属性。 【例5.2】下面的程序实现图5.2中电话类的继承结构。为了代码简洁,而把注意力集中在我们要讨论的语法机制上,这里假设移动电话仅一种,电话的计费方式简化为: 国内长途话费是市话费的3倍,国际长途话费是市话费的9倍。 import java.util.*; abstract class Telephone{ long phoneNumber; final static double local_Call=1; final static double distance_Call=3;//国内长途 final static double international_Call=9; //国际长途 double balance; abstract boolean charge_Mode(double call_Mode );//抽象方法 double getBalance() { return balance; } } class Mobile_Phone extends Telephone{ String networkType;//入网类型 String getType() { return networkType; } boolean charge_Mode (double call_Mode) { if (balance > 0.6) { balance -= 0.6;//移动电话为1分钟0.6元 return true;//可以打电话了 } else return false; } } abstract class Fixed_Telephone extends Telephone{ //固定电话 double monthFee; } class Ordinary_phone extends Fixed_Telephone { boolean longdistanceService; //国内长途服务是否开通 boolean internationalService; //国际长途服务是否开通 boolean charge_Mode(double call_Mode) {//call_Mode为1、3、9 if (call_Mode==distance_Call &&!longdistanceService)return false; //检查国内长途服务是否开通 if (call_Mode==international_Call &&!internationalService) return false; //检查国际长途服务是否开通 if (balance > (0.2 * call_Mode)) {//0.2是市话费 balance -= (0.2 * call_Mode);//市话费乘以相应的倍数 return true; } elsereturnfalse; } } class IP_Phone extends Fixed_Telephone { //IP电话 boolean started; Calendar expireDate; //Calendar是系统类,其对象代表一个具体的日期 boolean charge_Mode(double call_Mode) { if(!started) { started=true; expireDate = new GregorianCalendar(); //获得当前日期 expireDate.set(Calendar.MONTH,expireDate.get(Calendar.MONTH)+6); //设置6个月后过期 }; if (balance > 0.3 && expireDate.after(new GregorianCalendar())){ //new GregorianCalendar()创建一个包含当前日期的Calendar类的对象 //after()方法是Calendar类的方法,expireDate在当前日期之后时 //expireDate.after(new GregorianCalendar())返回true; if (call_Mode > local_Call){ balance -=(0.1*call_Mode+0.2); //用IP电话打国际长途和国内长途 //其中0.2是市话费 return true; } else return false;  //假定IP电话只打长途 }else return false; } } 例5.2定义了5个类,其中Mobile_Phone类和Fixed_Phone类是Telephone类派生的子类; IP_Phone类和Ordinary_phone类是Fixed_Phone类派生的子类。 在这个程序中,只在Telephone类中定义了phoneNumber、local_Call、distance_Call、international_Call、balance这些域,但是在Mobile_Phone、IP_Phone、Ordinary_phone类中使用了balance域和其他域,它们使用的这些域都是从父类Telephone继承的。 另外,Telephone类还定义了一个抽象方法charge_Mode(),由于它的子类Fixed_Telephone也是抽象类,可以不实现这个抽象方法。而其他派生的3个电话类不是抽象类,因此,分别根据自己的具体情况来实现charge_Mode()方法。例如,普通电话类Ordinary_phone的charge_Mode()方法要检查国内长途和国际长途服务是否开通,然后按通话类别计费。IP_Phone类的charge_Mode()方法定义的话费最低,但是要加上市话费,并且必须在失效日期之前拨打电话。而且,这些非抽象类必须实现charge_Mode()方法,否则编译时会出现错误。 现在检验一下这些类的定义是否可行,为此建立IP电话的测试类。在该类中建立一个IP电话的对象,再预置了50元话费; 然后分别将Telephone.international_Call和Telephone.distance_Call作为参数调用charge_Mode()方法,表示分别用IP电话打1分钟国际长途和用IP电话打1分钟国内长途。IP电话的测试类的代码如下。 class IP_PhoneTest { public static void main(String[] args) { IP_Phone ipp=new IP_Phone(); ipp.balance=50; ipp.charge_Mode(Telephone.international_Call); //用IP电话打1分钟国际长途 System.out.println("当前电话费的余额为"+ipp.balance); ipp.charge_Mode(Telephone.distance_Call); //用IP电话打1分钟国内长途 System.out.println("当前电话费的余额为"+ipp.balance); } } 该测试类的执行结果如图5.4所示。 图5.4IP电话的测试类的执行结果 5.2.2域的继承与隐藏 1. 域的继承 子类可以继承父类的所有非私有域。例如各类电话类所包含的域分别为: 1) Telephone类 long phoneNumber; final static double local_Call; final static double distance_Call; final static double international_Call double balance;//5个基本域 2) Mobile_Phone类 long phoneNumber; … double balance;  //来自继承父类Telephone的5个基本域 StringnetworkType; 3) Fixed_Phone类 long phoneNumber; … double balance;  //来自继承父类Telephone的5个基本域 double monthFee; 4) IP_Phone类 long phoneNumber; … double balance;   //来自继承父类Fixed_Telephone的5个基本域 double monthFee;   //来自继承父类Fixed_Telephone boolearn started; Date expireDate; 5) Ordinary_phone类 long phoneNumber; … double balance;  //来自继承父类Fixed_Telephone的5个基本域 double monthFee;  //来自继承父类Fixed_Telephone boolean longdistanceService; boolean internationalService; 可见父类所有的非私有域实际是各子类都拥有的域。子类从父类继承域而不需要重复定义父类的域,其好处是简化程序,降低维护的工作量。 2. 域的隐藏 上面提到子类可以从父类继承域而不需要重复定义父类的域,子类还可以重新定义一个与从父类继承来的域变量完全相同的变量,这种方式称为域的隐藏。即子类中定义了与父类同名的域变量,就是子类变量对同名父类变量的隐藏。这里所谓的隐藏是指子类拥有了两个相同名字的变量,一个来自继承父类,另一个由自己定义。在这种情况下,当子类执行继承的父类方法时,处理的是父类的变量,而当子类执行它自己定义的方法时,所操作的就是它自定义的变量,而把来自继承父类的变量“隐藏”起来了。 【例5.3】下面是一个测试域的隐藏的程序,其类层次结构如图5.5所示。 图5.5含有域的隐藏的类层次图 实现图5.5中含有域的隐藏的类层次图的代码如下。 //HiddenFieldExample.java abstract class Telephone{ long phoneNumber; final static double local_Call=1; final static double distance_Call=3; final static double international_Call=9; double balance; abstract boolean charge_Mode (double call_Mode); double getBalance() { return balance; } } class Mobile_Phone extends Telephone{ String networkType; String getType() { return networkType; } boolean charge_Mode(double call_Mode) { if (balance > 0.6) { balance -= 0.6; return true; } else return false; } } abstract class Fixed_Telephone extends Telephone{ double monthFee; } class Ordinary_phone extends Fixed_Telephone{ boolean longdistanceService; boolean internationalService; doublebalance;  //隐藏父类的balance boolean charge_Mode(double call_Mode) { if(call_Mode==distance_Call &&!longdistanceService) return false; if(call_Mode==international_Call &&!internationalService) return false;   //检查国内长途和国际长途服务是否开通 if(balance > (0.2 * call_Mode)) { balance -= (0.2 * call_Mode); return true; } else returnfalse; } } public class HiddenFieldExample{  //主类的定义 public static void main(String args[]){ Ordinary_phonemyfamilyphone=new Ordinary_phone(); myfamilyphone.internationalService=true; //对象myfamilyphone有两个balance变量, //一个来自继承父类,另一个是自定义的 myfamilyphone.balance=50.0; //为myfamilyphone自定义的balance变量赋值 System.out.println("父类被隐藏的金额为:" +myfamilyphone.getBalance()); //getBalance()方法返回的是myfamilyphone对象, //来自继承父类的balance的数值(没被赋值),其默认值是0.0 if (myfamilyphone.charge_Mode(Telephone.international_Call)) //调用myfamilyphone对象的charge_Mode()方法, //修改myfamilyphone对象自身的balance变量 System.out.println("子类的剩余金额为: "+myfamilyphone.balance); //输出修改后myfamilyphone对象自定义的balance变量值 } } 这个程序的执行结果如图5.6所示。 图5.6域的隐藏的测试结果 由于该程序在Ordinary_phone类中增加定义了一个与从父类那里继承来的变量完全相同的balance变量。这样修改后,Ordinary_phone类中的域变为: long phoneNumber; … double balance;  //来自继承父类Fixed_Telephone的5个基本域 double balance;   //Ordinary_phone类自定义的域 double monthFee;  //来自继承父类Fixed_Telephone boolean longdistanceService;  //Ordinary_phone类自定义的域 boolean internationalService;  //Ordinary_phone类自定义的域 此时子类有两个同名变量balance,一个来自继承父类,另一个由自己定义,当执行不同操作时,处理不同的变量。 5.2.3方法的继承与覆盖 1. 方法的继承 父类的非私有方法作为类的非私有成员,可以被子类所继承。根据方法的继承关系,将例5.3中的电话类及其各子类所包含的方法列举如下。 1) Telephone类 abstract boolean charge_Mode(); double getBalance(); 2) Fixed_Telephone类 abstract boolean charge_Mode(); //来自继承父类Telephone double getBalance(); //来自继承父类Telephone 3) Ordinary_phone类 double getBelance(); //来自继承父类Fixed_Telephone booleancharge_Mode(); 各个类的对象可以自由使用从父类继承的方法。 2. 方法的覆盖 方法的覆盖(Override)是指子类重新定义从父类继承的同名方法,此时父类的这个方法在子类中将不复存在。这是子类通过重新定义与父类同名的方法,实现自身的行为。 【例5.4】方法的覆盖程序举例。 import java.awt.*; import java.applet.Applet; classaaa{ double f(double x,double y) { returnx*y; } } classbbbextendsaaa{ double f(double x, double y){//覆盖父类的方法 returnx+y; //如果注释掉本方法,则不存在覆盖 } } public class OverrideExample extends Applet{ bbbobj; public void init(){ obj=new bbb(); } public void paint(Graphics g){ g.drawString("the object is "+obj.toString(), 5,20); g.drawString("the program's output is "+obj.f(4,6), 5,40); } } 在这个程序的bbb类中,由于覆盖了父类的方法,bbb类的对象obj调用的是自定义的方法,其执行结果如图5.7(a)所示。假如在这个程序的bbb类中将f方法注释掉,这个程序仍然可以执行,这时候的执行结果如图5.7(b)所示。分析一下结果就不难看出它们各自执行的是哪个类的f方法。 图5.7方法的覆盖 方法的覆盖中需要注意的问题是: 子类在重新定义父类已有的方法时,应保持与父类完全相同的方法头部声明,即应保持与父类有完全相同的方法名、返回类型和参数列表,否则就不是方法的覆盖,而是子类定义了自己的、与父类无关的方法,父类的方法仍然存在于子类中。 图5.8同时含有隐藏与覆盖的 示意说明 方法的覆盖与域的隐藏的不同之处在于: 子类隐藏父类的域只是使之不可见,父类的同名域在子类对象中仍然占有自己独立的内存空间; 而子类方法对父类同名方法的覆盖将清除父类方法占用的内存空间,从而使父类方法在子类对象中不复存在。 域的隐藏和方法的覆盖的意义在于: 通过隐藏域和覆盖方法可以把父类的状态和行为改为自身的状态和行为,对外统一名字与接口,又不失其继承性。 图5.8进一步给出了同时含有域的隐藏和方法的覆盖的示意说明。假设在主类中,建立Rect类的对象c,若有调用: (1) c.area(),这是在使用Rect类的area()操作自己的x、y,求矩形面积。 (2) c.getXY(),这是在使用继承父类Point的getXY()处理所继承父类的x、y。 视频讲解 5.3多态性 多态性是面向对象程序设计的又一个重要的技术和手段。多态性是指同名的不同方法在程序中共存,即为同一个方法定义几个版本,运行时根据不同情况执行不同的版本。调用者只需使用同一个方法名,系统会根据不同情况,调用相应的不同方法,从而实现不同的功能。多态性又被称为“一个名字,多个方法”。 5.3.1多态性的概念 在面向过程的程序设计中,各函数是不能重名的,否则在用名字调用时就会产生歧义和错误。而在面向对象的程序设计中,有时却需要利用这样的“重名”现象来提高程序的抽象度和简洁性。 考查图5.2中的电话类的层次树,计费方式是所有电话都具有的操作,但不同的电话计费方式操作的具体实现是不同的。如果不允许这些目标和功能相同的操作使用同样的方法名字,就必须分别用多个不同名字的方法定义普通电话、IP电话和移动电话类的计费方式。这样使用起来不方便,要记忆和区分多个方法的名字,继承的优势就不明显了。在面向对象的程序设计中,为了解决这个问题,引入了多态性的概念。 多态性的实现有以下两种。 1) 覆盖实现多态性 通过子类对继承父类方法的重定义来实现。使用时注意: 在子类重定义父类方法时,要求与父类中方法的原型(参数个数、类型、顺序)完全相同。 2) 重载实现多态性 通过定义同一个类中的多个同名的不同方法来实现。编译时是根据参数(个数、类型、顺序)的不同来区分不同方法的。 5.3.2覆盖实现多态性 第一种多态,即通过子类对父类方法的覆盖实现的多态。以图5.2中各类电话为例,Telephone类有一个各子类共有的方法charge_Mode()代表计费方式的功能,根据继承的特点,Telephone的每个子类都将继承这个方法。但是,该方法的功能在不同种类的电话中其具体实现是不同的。因此,不同的子类可以重新定义、编写charge_Mode()的内容,以实现特定计费方式的方法。在这些子类中,虽然实现的方式不同,但却共享同一个方法名——charge_Mode()。 既然在覆盖实现多态性的方式中,子类重定义父类方法时,方法的名字、参数个数、类型、顺序完全相同,那么如何区别这些同名的不同方法呢?由于这些方法存在于一个类层次结构的不同类中,在调用方法时只需要指明调用哪个类(或对象)的方法,就很容易选择正确的版本,其调用形式为: 对象名.方法名(参数表) 类名.方法名(参数表) 例如,IP电话的计费,若建立IP_Phone类的对象my,其调用为: my.charge_Mode(); 假如charge_Mode()是一个类方法,则可以使用类名调用: IP_Phone.charge_Mode(); 运行时,系统会根据不同的调用者分辨所使用的是哪种类型的电话,并调用相应的具体计费方式的方法,从而实现计费方式功能的多态性。 【例5.5】这个例子展示了使用父类对象和子类对象调用同名方法时,不同的调用结果。 import java.applet.*; import java.awt.*; class Area{ double f(double r){ return3.14*r*r; } double g(double x,double y){ return0.5*x*y; } } class Circle extends Area{ double f(double r){ return3.14*2.0*r; } } public class PolyMorphism_Example1 extends Applet{ Areaobj;  //父类对象 Circlecir;  //子类对象 public void init() { obj=new Area(); cir=new Circle(); } public void paint(Graphics g){ g.drawString("圆面积: "+obj.f(5.0),5,20); //调用父类的方法f求圆面积 g.drawString("圆周长: "+cir.f(5.0),5,40); 图5.9第一种多态的实例 //调用子类的方法f求圆周长 g.drawString("三角形面积: "+cir.g(2.0,8.0),5,60); //调用所继承父类的方法g求三角形面积 } } PolyMorphism_Example1类的执行结果如图5.9所示。 5.3.3重载实现多态性 第二种多态性通过重载来实现,它是在同一个类中定义多个同名方法,这些方法同名的原因是具有相同的功能且目的相同,但在实现该功能的具体方式和细节方面则有所不同,因此需要定义多种不同的实现方法。例如,加法是属于一类操作,目的是把两个数通过“相加”变成一个数。但不同数加法的具体实现是不一样的,其中差别较大的有整数的加法、复数的加法和分数的加法。利用重载,可以给这些加法都取名为add(x, y),然后再分别定义整数、复数和分数加法的具体实现。 由于重载发生在同一个类中,不能再用类名或对象名来区分不同的方法了,所以在重载中采用的区分方法是使用不同的形式参数表,包括形式参数的个数不同、类型不同或顺序的不同。例如,在加法中,整数加法的形参类型是整型,复数加法的形参类型是复数型。在具体实施加法时,尽管调用的是同一个方法名,但根据填入的参数的类型或者参量等的不同,系统可以确定调用哪一个加法函数来完成加法计算。这样,使用时只需用不同的参数调用add(x,y),即可实现不同类型的加法运算。 如何实现重载在4.4.4小节中已有详细的叙述,下面补充一个实例。 【例5.6】普通电话的计费方式通常包括在节假日和晚上某个时间段打折,也就是说在普通电话类中就至少包含两种计费方式。因此,可定义两个同名的不同charge_Mode()方法,不同情况下调用不同的方法。为了简单起见,设置一个布尔变量discount_time,需要计费打折时discount_time为真。重载一个charge_Mode()方法的程序如下。 abstract class Telephone{ long phoneNumber; final static double local_Call=1; final static double distance_Call=3; final static double international_Call=9; double balance; abstract boolean charge_Mode(double call_Mode); double getBalance() { return balance; } } class Mobile_Phone extends Telephone{ String networkType; String getType() { return networkType; } boolean charge_Mode(double call_Mode) { if(balance > 0.6) { balance -= 0.6; return true; } else return false; } } abstract class Fixed_Telephone extends Telephone { double monthFee; } class Ordinary_phone extends Fixed_Telephone{ boolean longdistanceService; boolean internationalService; boolean charge_Mode(double call_Mode) { if(call_Mode==distance_Call &&!longdistanceService) return false; //检查国内长途服务是否开通 if(call_Mode==international_Call &&!internationalService)return false; //检查国际长途服务是否开通 if(balance > (0.2 * call_Mode)) { balance -= (0.2 * call_Mode); return true; } else returnfalse; } boolean charge_Mode(double call_Mode, boolean discount_time ){ if (discount_time)//如果是在打折的时间 returncharge_Mode(call_Mode/2);//收费减半 else returncharge_Mode(call_Mode);  //正常时间的计费方式 } } publicclassReloadExample{  //主类定义 public static void main(String args[]){ Ordinary_phone myOfficePhone=new Ordinary_phone(); myOfficePhone.internationalService=true; myOfficePhone.balance=500.0; if (myOfficePhone.charge_Mode(Telephone.international_Call,true)) //调用第二个charge_Mode方法 System.out.println("半价计费方式后剩余金额为: " +myOfficePhone.getBalance()); if (myOfficePhone.charge_Mode(9)) //调用第一个charge_Mode方法 System.out.println("一般计费方式后剩余金额为: " +myOfficePhone.getBalance()); } } 程序运行结果如图5.10所示。 图5.10两种计费方式计费后的余额 图5.10中表明,用半价方式通话1分钟消耗话费0.9元,再用普通方式通话1分钟,消耗话费1.8元。仔细分析这个程序,我们会发现一个有趣的现象: 在Ordinary_phone类中定义的charge_Mode()方法实现了父类的同名抽象方法,接着又定义一个同名方法重载这个方法。 5.4构造方法的继承与重载 因为构造方法的特殊性,所以我们在这里单独讨论它的继承与重载问题。 5.4.1构造方法的重载 构造方法的重载是指同一个类中定义不同参数的多个构造方法,以完成不同情况下对象的初始化。例如,对于表示平面坐标中的点的Point类,可定义不同的构造方法创建不同的点对象。 Point(); //未初始化坐标 Point(x); //初始化一个坐标 Point(x, y); //初始化两个坐标 一个类的若干构造方法之间可以相互调用。当类中一个构造方法需要调用另一个构造方法时,可以使用关键字this(),括号中可带也可不带参数,并且这个调用语句应该是该构造方法的第一个可执行语句。 【例5.7】前面没有定义Ordinary_phone类的构造方法,我们可以根据需要定义几个构造方法: Ordinary_phone()//无参数的构造方法,对象的各域均置为默认初始值 {} Ordinary_phone(boolean disService) {  //一个参数的构造方法 this();  //调自身的无参数的构造方法 longdistanceService=disService;  //确定是否开通国内长途电话 } Ordinary_phone(boolean disService, boolean intService) {   //两个参数的构造方法 this(disService);  //调自身的带一个参数的构造方法 internationalService = intService;  //确定是否开通国际长途电话 } Ordinary_phone(boolean disService, boolean intService, double b) {   //3个参数的构造方法 this(disService, intService);  //调自身的带两个参数的构造方法 balance=b;  //设置话费金额 } 使用this关键字调用同类的其他构造方法,其优点是可以最大限度地提高对已有代码的利用程度,提高程序的抽象、封装程度,以及减少程序维护的工作量。 视频讲解 5.4.2构造方法的继承 子类可以继承父类的构造方法,构造方法的继承遵循以下原则。 (1) 子类无条件地继承父类的无参数构造方法。 (2) 如果子类没有定义构造方法,则它将继承父类的无参数构造方法作为自己的构造方法; 如果子类定义了构造方法,则在创建新对象时,将先执行来自继承父类的无参数构造方法,然后再执行自己的构造方法。 (3) 对于父类的带参数构造方法,子类可以通过在自己的构造方法中使用super关键字来调用它,但这个调用语句必须是子类构造方法的第一个可执行语句。 下面将分别对这3个原则举例说明。 1. 若有子类,父类一定要有无参构造方法给子类继承 前面谈到,子类无条件地继承父类的无参数的构造方法。这时一个类若有子类,这个类一定要有无参构造方法给子类继承。 【例5.8】父类没有定义无参构造方法给子类继承。 下面这个类没有定义无参构造方法,因此在有子类继承时会出现问题。父类和子类的代码如下。 class A{ public A(int a){ System.out.println(" String a" +a ); } } class B extends A{ 图5.11父类没有定义无参构造方法给子类继承时 出现的编译错误 public B(){ System.out.println(" String b"); } } public class ConstructorInheritance{ public static void main(String args[]){ B b = new B(); } } 在编译这个程序时显示如图5.11所示的信息。 要消除这个错误,类A中必须增加一个无参构造方法。例如,增加如下构造方法就不再出现编译错误: public A(){ System.out.println(" String a"); } 此时,子类就可以无条件地继承父类的无参数的构造方法。 当然如果子类不使用无参构造方法,父类也可以没有无参构造方法。例如,下面的程序就是一个合法的程序。 class A{ public A(int a){ System.out.println(" String a" +a ); } } class B extends A{ public B(int a){ super(a); System.out.println(" String b"); } } 这个程序主要是B类的构造方法中有super(a)语句,该语句的作用是调用父类的构造方法。假如以上程序没有这个语句,该程序也难以通过编译。下面还会进一步给出该语句的实例。 2. 父类与子类的构造方法的执行顺序 对于上面5.4.2小节中的第(2)点,下面给出一个简单示意性的例子。 【例5.9】试分析下面程序的继承关系以及构造方法的调用顺序。我们是否可以先不看执行结果,分析构造方法的执行顺序是A()、B()、C()还是C()、B()、A()? class A{ public A(){ System.out.println(" String a"); } } class B extends A{ public B(){ System.out.println(" String b"); } } class C extends B{ public C(){ System.out.println(" String c"); 图5.12有继承关系的类构造 方法的执行顺序 } } public class ConstructorTest{ public static void main(String args[]){ C c = new C(); } } ConstructorTest类的执行顺序如图5.12所示。 3. 在构造方法中super关键字的使用 super是表示父类对象的关键字,super表示当前对象的直接父类,代表当前父类对象的一个引用,其作用是利用super可使用父类对象的方法或域。 请看下面的例子,假设父类Fixed_Telephone有4个构造方法。 Fixed_Telephone()  //无参数的构造方法,对象的各域均置为默认初始值 { } Fixed_Telephone(long pn) { phoneNumber = pn;  //初始化电话号码 } Fixed_ Telephone(long pn, double mf) { phoneNumber = pn; monthFee = mf;  //初始化座机费 } Fixed_Telephone(long pn, double mf, double b) { phoneNumber = pn; monthFee = mf; balance= b;  //初始化电话费的话费余额 } 设计子类的构造方法可选择如下方式。 (1) 仅调用父类的无参数构造方法。 (2) 定义自己的一个(或多个)构造方法并调用父类的带参数的构造方法。 根据上述方法,子类Ordinary_phone的构造方法设计如下。 Ordinary_phone(long pn, double mf, boolean ds) { super(pn, mf);  //调用父类两个参数的构造方法为继承的域赋初值 longdistanceService = ds;  //用参数初始化自定义域 } Ordinary_phone(long pn, double mf, double b, boolean ds){ super(pn,mf, b);  //调用父类3个参数的构造方法为继承的域赋初值 longdistanceService = ds;  //用参数初始化自定义域 } 按照这种方式,我们可以很方便地定义各种构造方法。 5.4.3重载和覆盖的综合举例 【例5.10】构造方法的继承与重载以及方法的覆盖的综合举例。 abstract class Telephone{ long phoneNumber; final static double local_Call=1; final static double distance_Call=3; final static double international_Call=9; double balance; abstract boolean charge_Mode(double call_Mode); double getBalance(){ return balance; } } abstract class Fixed_Telephone extends Telephone{ double monthFee; Fixed_Telephone()//构造方法的重载 {} Fixed_Telephone(long pn) { this(); phoneNumber=pn; } Fixed_Telephone(long pn, double mf) { this(pn); monthFee=mf; } Fixed_Telephone(long pn,double mf,double b) { this(pn,mf); balance=b; } } class Ordinary_phone extends Fixed_Telephone{ boolean longdistanceService; boolean internationalService; Ordinary_phone(long pn,double mf,boolean a){ super(pn, mf); longdistanceService=a; } Ordinary_phone(long pn,double mf,double b,boolean a){ super(pn,mf, b); longdistanceService=a; } boolean charge_Mode(double call_Mode) {//覆盖 if (call_Mode==distance_Call &&!longdistanceService) return false; if (call_Mode==international_Call &&!internationalService) return false;   //检查国内长途和国际长途服务是否开通 if (balance > (0.2 * call_Mode)){ balance -= (0.2 * call_Mode); return true; } else returnfalse; } boolean charge_Mode(double call_Mode, boolean discount_time){ if (discount_time){  //如果是在打折的时间 returncharge_Mode(call_Mode/2); } else returncharge_Mode(call_Mode);  //正常时间的计费方式 } double getBalanee(){ //覆盖Telephone的对应方法 if (balance > monthFee) return balance - monthFee; else return -1; } public String toString() { //覆盖Object的对应方法 String yn = longdistanceService ? "是":"否"; return ( "电话号码为:"+phoneNumber +"\n 每月座机费:"+monthFee +"\n 剩余金额:"+balance +"\n 可通国内长途:"+yn); } } public class SumaryExample{  //定义主类 public static void main(String args[]){ Ordinary_phone myHomePhone= new Ordinary_phone(87688888,25.0,100.0, true); System.out.println(myHomePhone.toString()); } } 图5.13Ordinary_phone类的一个对象的初始化后的结果 本例中使用重载技术定义了Fixed_TelephoneCsrd类的4个构造方法; 使用继承和重载技术定义了Ordinary_phone类的两个构造方法; 使用覆盖技术在Ordinary_phone类中覆盖了父类的getBalance()方法、charge_Mode()方法以及Object类的toString()方法。主类中创建类的对象myHomePhone时使用了第二个构造方法,并对大部分的域都进行了初始化,图5.13输出了初始化后的结果。 5.5接口 接口(interface)也有人翻译为界面,是用来实现类间达成共性和多重继承功能的一种结构,是相对独立的特定功能的属性集合。凡是需要实现这种特定功能的类,都可以继承并使用它。一个类只能直接继承一个父类,但可以同时实现若干接口。利用接口实际上就获得了多个特殊父类的属性,即实现了多重继承。 Java的接口是用来组织应用中的各种类和调节它们之间的相互关系的一种结构,在语法上与类有些相似。它定义了若干抽象方法和常量,形成一个属性集合,该属性集合通常对应了某一组功能。接口定义的仅是实现某特定功能的一组对外接口和规范,而这个功能的真正实现是在继承这个接口的各类中完成的。 5.5.1接口与多继承 Java只支持单重继承,即一个类至多只能有一个直接父类。接口的主要作用就是可以帮助实现类似于类的多重继承的功能。所谓多重继承,是指一个子类可以有一个以上的直接父类,该子类可以继承它所有的直接父类的属性。某些面向对象的语言(如C++)提供多重继承的语法级支持。而在Java中,出于简化程序结构的考虑,不直接支持类间的多重继承。但在解决实际问题的过程中,往往又需要这种机制。 由于Java只直接支持单重继承,所以Java程序中的类层次结构是树状结构,这种树状结构在随着类结构树的生长,越是处在下层的子类,它的间接父类就越多,所以继承的域及方法也会越来越多,造成子类成员的膨胀、庞杂。 为了使Java程序的类层次结构更加合理,更符合实际问题的本质,可以把用于完成特定功能的若干属性(抽象方法和常量)组织成相对独立的属性集合,凡是需要实现这种特定功能的类,都可以继承这个属性集合并在类中使用它,这种属性集合就是接口。 需要特别说明的是,接口也可以认为是这些类之间行为的协议。但它只是定义了行为的协议,没有定义履行接口协议的具体方法。如果Java中的一个类要获取某一接口定义的功能,并不是通过直接继承这个接口中的属性和方法来实现的,因为接口中的属性都是没有方法体的抽象方法,接口定义的仅仅是实现某一特定功能的一组对外的协议和规范,并没有真正地实现这个功能。这些功能的真正实现是在继承这个接口的各个类中完成的,由这些类来具体定义接口中各抽象方法的方法体,以适合某些特定的行为。因而在Java中,通常把对接口功能的继承下来后具体实施的过程称为“实现”(implement)。 接口包含的是未实现的一些抽象的方法,它与抽象类有些相似。研究一下接口与抽象类到底有什么区别是很有意义的。它们之间存在以下的区别。  接口不能有任何实现了的方法,而抽象类可以。  接口不能有任何变量,而抽象类可以。  接口可以继承(实现)多个接口,但抽象类只能继承一个父类。  类有严格的层次结构,而接口不是类层次结构的一部分,没有联系的类可以实现相同的接口。 5.5.2接口的定义 接口是由常量和抽象方法组成的特殊类。定义一个接口与定义一个类是类似的。接口的定义包括两个部分: 接口声明和接口体。定义接口的一般格式如下。 [public] interface 接口名 [extends 父接口名表] { 域类型域名=常量值;  //常量域声明 返回值方法名(参数表);  //抽象方法声明 } 接口声明中有两个部分是必需的: interface关键字和接口的名字。用public修饰的接口是公共接口,可以被所有的类和接口使用; 没有public修饰符的接口则只能被同一个包中的其他类和接口使用。 像类之间可以继承一样,接口也具有继承性,子接口可继承父接口的所有属性和方法。但是,类只能继承一个父类,而接口可以继承多个父接口。在“父接口名表”中以逗号分隔所有的父接口名,这些接口可以被新的接口所继承。图5.14给出接口的一个实例,并给出了这个接口声明的各个部分的含义。 图5.14StockWatcher接口的各个部分及其含义 该接口定义了3个常量,它们是所监视的股票代码。这个接口还定义了valueChanged()方法。实现这个接口的类将为这个方法提供具体的实现。 因为所有定义在接口中的常量都默认为public、static和final,所有定义在接口中的方法默认为public和abstract,所以不需要用修饰符限定它们。 假如已经编写了一个StockMonitor类,这个类的功能是监督股票的价格。它可以执行一个方法让其他的对象注册,以便得到通知。该类允许其他的类调用它的watchStock()方法,从而知道什么时候特定的股票的价格发生改变。 public class StockMonitor{ public void watchStock(StockWatcher watcher, String tickerSymbol, double delta) {…} } 这个方法的第一个参数watcher为StockWatcher对象。watcher对象所属的类必须实现StockWatcher接口。其他两个参数提供了股票的代码和观察改变的数目。当StockMonitor类检测到一个感兴趣的变化时,它就会调用watcher对象的valueChanged()方法。watchStock()方法通过第一个参数的数据类型确保所有替代watcher参数的对象实现valueChanged()方法。 通过使用接口类型作为参数,替代watcher参数的对象类可以是Applet或者Thread等各种类型的类。 5.5.3接口的实现 为了使用接口,要编写实现接口的类。如果一个类实现一个接口,那么这个类就应提供在接口中定义的 视频讲解 所有抽象方法的具体实现。 一个类可以根据定义在接口中的协议来实现接口。为了声明一个类来实现某一个接口,在类的声明中要包括一条implements语句。因为Java支持接口的多继承,一个类可以实现多个接口,因此可以在implements后面列出要实现的多个接口,这些接口以逗号分隔。 以下是一个Applet类,它实现StockWatcher接口。 public class StockApplet extends Applet implements StockWatcher{ … public void valueChanged(String tickerSymbol, double newValue){ if (tickerSymbol.equals(sunTicker)) { … } else if (tickerSymbol.equals(oracleTicker)) { … } else if (tickerSymbol.equals(ciscoTicker)) { … } } } 这个类引用了定义在StockWatcher接口中的常量,如oracleTicker、sunTicker等。因为实现接口的类继承了接口中定义的常量,所以可以使用一般的变量名字来引用常量,也可以用下面的语句的方式,在其他任何类中使用接口常量: StockWatcher.sunTicker StockApplet实现StockWatcher接口,因此它应提供valueChanged方法的实现。当一个类实现一个接口中的抽象方法时,这个方法的名字和参数类型及数目必须与接口中的方法匹配。 下面归纳一下实现接口时应注意的问题。 (1) 在类的声明部分,用implements关键字声明该类将要实现哪些接口。 (2) 类在实现抽象方法时,必须用public修饰符。 (3) 除抽象类以外,在类的定义部分必须为接口中所有的抽象方法定义方法体,且方法首部应该与接口中的定义完全一致。 (4) 若实现某接口的类是abstract的抽象类,则它可以不实现该接口所有的方法。但是对于这个抽象类的任何一个非抽象子类,不允许存在未被实现的接口方法,即非抽象类中不能有抽象方法存在。 5.5.4接口的使用 定义一个新的接口的时候,实际上是定义了一个新的引用数据类型。在可以使用其他类型的名字(如变量声明、方法参数等)的地方,都可以使用这个接口名。例如,前面在StockMonitor类中的watchStock()方法中的第一个参数的数据类型为StockWatcher接口。只有实现StockWatcher接口的类对象可以替代watcher形参。 此外,应该注意: 接口不能被覆盖,即不能有多个版本。假如想在StockWatcher中增加一个收集当前股票价格的方法,于是,试图定义一个新的版本: public interface StockWatcher{ final String sunTicker = "SUNW"; final String oracleTicker = "ORCL"; final String ciscoTicker = "CSCO"; void valueChanged(String tickerSymbol,double newValue); void currentValue(String tickerSymbol,double newValue); } 如果做了这个改变,实现老版本的StockWatcher接口的所有类的继承和实现关系都将中断,因为它们没有实现这个接口的所有方法。 为了达到以上增加一个方法的目的,可以创建新的接口来继承老接口。例如,可以创建一个StockWatcher的子接口StockTracker: public interface StockTracker extends StockWatcher{ void currentValue(String tickerSymbol,double newValue); } 这就可以避免了上面提及的问题。 5.5.5接口的完整实例 上面给出的例子属于示意性的例子,对于说明某一个部分的语法机制很有作用。下面给出使用接口的一个完整实例。程序的讲解穿插在程序中的注解行中。 【例5.11】接口应用的完整实例。 //接口的声明 interface Speaker{ public void speak(); public void announce(String str); } //接口的实现 class Philosopher implements Speaker{ private String philosophy; //初始化哲学家的哲理 public Philosopher(String philosophy) { this.philosophy = philosophy; } //"唠叨"哲学家的哲理 public void speak(){ System.out.println(philosophy); } //发表一个宣言 public void announce(String announcement){ System.out.println(announcement); } //反复"唠叨"哲学家的哲理 public void pontificate(){ for (int count=1; count <= 5; count++) System.out.println(philosophy); } } //接口的实现 class Dog implements Speaker{ //发表狗的哲理 public void speak (){ System.out.println ("woof"); } //发表狗的哲理和宣言 public void announce (String announcement) { System.out.println ("woof: " + announcement); } } //演示使用一个接口多态性 class Talking{ //初始化Speaker接口的一个引用 //先后指向两个不同类的对象,调用它们的公共方法 public static void main (String[] args) { Speaker current; current = new Dog(); current.speak(); current = new Philosopher("I think, therefore I am."); current.speak(); ((Philosopher) current).pontificate(); } } 程序中声明了接口Speaker的一个引用current,用它先后指向两个不同的类对象,分别调用它们的公共方法speak(),这两次调用分别使用的是不同的方法体,因为这是由两个类提供了speak()方法的不同实现。在调用时,系统可以根据不同的对象找到正确的调用,这就是由接口实现的继承的多态性。前面我们在电话类及其子类看到抽象类的抽象方法charge_Mode()具有多态性,这个例子展示了接口也有类似的功能。 图5.15完整地实现一个接口 后的执行结果 另外还要指出,在这个程序最后一行,不能直接用current.pontificate()进行调用,因为current属于Speaker类型,而通过Speaker接口只能请求到该接口所包含的方法的调用。因此,在上面的一个语句中可以通过current对象调用speak()方法,而在这里必须将current进行类型转换(cast),变成Philosopher的对象,再调用pontificate()方法(pontificate一词的意思是: 装作教皇说话的样子)。上述程序的执行结果如图5.15所示。 5.6利用抽象类、接口和Object类 实现多态性下的计算 现在提出一个比较特别的应用,要计算大量的(如1000个)多种形状的面积之和,这些形状是随机产生的, 视频讲解 即不知道什么时候产生什么样的形状。设定这样的条件是为了打消在已知图形的形状的情况下,使用一个一个地、按图形类别来计算面积的方式将结果进行累加的念头,也就是说必须用循环的方式进行处理。 下面讨论用抽象类、接口和Object类实现多种形状图形的面积累加。 5.6.1用抽象类实现多种形状面积的累加 首先,定义Shape、Rectangle、Circle和Square类,要利用多态性,确保每种形状分别用不同的方法来计算它们的面积。因此,定义Shape类作为超类,该类包含抽象方法computeArea,然后,在其子类中实现和覆盖这个方法,下面是这些类的实现。 public abstract class Shape {protected double area; public abstract void computeArea(); } public class Rectangle extends Shape {protected double width, height; public Rectangle(double _width, double _height) {width = _width; height = _height; } public void computeArea() {area = width * height; } } public class Circle extends Shape {public Circle(double _radius) {type = "Circle"; radius = _radius; } public void computeArea() {area = Math.PI * radius * radius; } } public class Square extends Rectangle {public Square(double _side) {super(_side, _side); } } 有了上面这些类,就很容易创建它们的实例,并利用多态性选择适当的方法来计算。下面再定义一个类ShapeTestWithAbstract,该类的main方法中声明了1000个形状对象的Shape数组,然后循环1000次随机产生1000个平面图形对象,形状为圆、矩形或正方形。该类的代码如下。 public class ShapeTestWithAbstract {public static void main(String args[]) {Shape shapes[]=new Shape[1000]; double total_area=0.0; int k=0; for (int i=0; i