1、第 5 章 继承和多态继承和多态性是面向对象程序设计的重要内容。继承机制是实现软件构件复用的一种强有力的手段。多态性是面向对象编程的重要特性,是继承产生的结果。Java 语言很好的体现了继承和多态性两大特性。本章将讨论用 Java 语言实现继承和多态性,具体将介绍继承概念、继承的实现、抽象类的作用、方法的覆盖以及用接口实现多继承。5.1 继承继承是面向对象程序设计的三大概念之一,是面向对象程序的重要概念,它使程序代码复用成为可能。假设已经定义和实现类 DigitalProduction(代表数码产品) ,需要定义一个新的应用类 MobilePhone(代表手机) 。由于手机是数码产品的一种,所
2、以没有必要对类DigitalProduction 中属于数码产品特征重新书写代码,只需要继承类 DigitalProduction 的属性特征即可。这样,类 MobilePhone 通过继承获得了类 DigitalProduction 的数据和方法。由于类 MobilePhone 也具有自身的特征,如“进网许可号” ,可以为这些异于其他数码产品的特征定义成员数据和成员方法。这样,MobilePhone 继承于 DigitalProduction,又具有新的属性特征。图 5-1 可以表示了两个类之间的继承关系。图 5-1 MobilePhone 继承 DigitalProduction从这个意义
3、上来说,继承是指一个新的类继承原有类的基本特性,并增加新的特性。通俗地说,新的类与原有类之间体现了一种“is-a”关系。只要类和类之间存在的继承关系,这种扩展可以一直延续下去,它体现出类的层次结构。例如,而类 MobilePhone 继承于类DigitalProduction。而类 IntelligentMobile(代表智能手机)继承于类 MobilePhone。继承可以分成两大类型:单继承和多继承。单继承是一个类只能从一个类派生而来,即只有一个父类。多继承是一个类可以从多个类派生而来,可以有多个父类。Java 语言只支持单继承,不支持多继承。5.1.1 父类和子类Java 语言中,继承实际
4、上是一个类扩展一个已有的类。被扩展的类是父类,而扩展类是子类。子类继承了父类的类成员,并可以定义自己的独特的属性成员。通常体现了子类和父类之间是派生与被派生的关系。所以,有时称父类为基类,而子类称为派生类。Java 语言也体现出类的层次结构。Java 语言中定义类 java.lang.Object,它是所有类的父类。其他类无论是直接还是间接都是继承了 Object 类,具有 Object 类的属性,如在第 4 章中说明的 finalize()方法。比如, Java 语言提供的类 java.lang.String 直接继承于类java.lang.Object,而 javax.swing.JOpt
5、ionPane 则是间接继承于 java.lang.Object,JOptionPane类具体继承情况见图 5-2。同样,用户自定义类也是直接或间接继承于 java.lang.Object 类,具体实现见 5.1.2。图 5-2 javax.swing.JOptionPane 的继承示意5.1.2 继承的实现Java 语言中是通过关键字 extends 来实现单继承的。简单的实现格式如下:class 子类名 extends 父类名类体有一点要注意,如果在格式中没有通过 extends 关键字来标明父类名,这并不是意味着该类无父类,相反,它表示该类是 java.lang.Object 的子类。例
6、 5.1 类继承的示例。public class BaseClass /定义一个类 BaseClasspublic int intValue; /定义成员数据 intValue;public BaseClass()intValue=3;System.out.println(“Base Class 定义“);public void showBaseMessage()System.out.println(“Base Class 信息“);public class DerivedClass extends BaseClass/定义类 DerivedClasspublic DerivedClass()
7、System.out.println(“Derived Class 定义“);public void showDerivedMessage()System.out.println(“继承而来的数据: “+intValue);/引用继承的成员数据 intValueSystem.out.println(“Derived Class 信息“);public static void main(String args)DerivedClass dc=new DerivedClass();dc.showBaseMessage();/引用继承的成员方法 showBaseMessagedc.showDeriv
8、edMessage();System.exit(0);图 5-3 例 5.1 的运行结果例 5.1 定义了两个类 BaseClass 和 DerivedClass,其中 DerivedClass 是 BaseClass 的子类,BaseClass 是 DerivedClass 的父类。它们之间通过定义 DerivedClass 中增加 extends 子句来实3现继承关系。尽管类 BaseClass 的定义没有用关键字 extends 来说明父类,但实际上它是隐性的定义为 java.lang.Object 的子类。这样,java.lang.Object、BaseClass 和 DerivedC
9、lass 之间就构成了继承关系链。创建一个子类如 DerivedClass 的对象时,会从上向下调用 Object-BaseClass-DerivedClass 继承链的默认构造方法。从图 5-3 中可以看出,创建一个DerivedClass 的对象之前先调用了父类 BaseClass 的默认构造方法。注意,如果在继承链中存在某个父类没有定义默认构造方法,会产生编译错误。不过,尽管子类可以继承父类的成员数据和成员方法,但并不意味着子类可以完全继承父类的全部属性。如果将例 5.1 中类 BaseClass 的公有(public)成员数据 intValue 改成私有(private)成员数据,再编
10、译类 Derived,会发现程序出现编译错误。这是由于类成员的访问控制限制了类成员的可见性。父类的私有成员是不能被子类所继承。就好比孩子继承了父亲的部分特征,而不是全部特征。5.2 抽象类在现实生活中,可以发现这样的现象:许多实物抽象出成为一个共同的类别,如“交通工具” ,但是“交通工具”并直接对应着实际存在的类别如卡车、自行车、三轮车等。又如“动物” ,并不特指具体的某个实际存在,它只是哺乳动物、两栖动物等以有机物为食物,可运动生物的统称。 “交通工具” 、 “动物”等这些只是作为一个抽象概念存在,对一组固定实现的抽象描述,并不对应具体的概念。因此,在面向对象程序设计中,可以将这些抽象描述定
11、义为抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。由于抽象类对一组具体实现的抽象性描述,所以抽象体是不能修改的。通常抽象类不具备实际功能,只用来派生子类。5.2.1 抽象方法Java 语言中,用关键字 abstract 修饰的方法为抽象方法。语法格式形如:abstract 返回值类型 方法名(形式参数) ;例如: abstract void showMessage();抽象方法只有方法头没有方法体,即只提供方法的规格说明,没有具体的实现。抽象方法只能出现在抽象类(见 5.2.2)和接口(见 5.4)中,其他类中定义会产生编译错误。有两点必须注意:1)final 关键字不能修饰抽
12、象方法。这是因为 final 是不可修改的方法是最终的方法,而抽象方法没有方法体,能被派生类进行覆盖;2)static 是不能修饰抽象方法的。静态方法占据固定内存空间,而抽象方法根本就不可能运行,不会加载到内存中。所以,static 关键字不能修饰抽象方法。5.2.2 抽象类抽象类是用关键字 abstract 修饰的类为抽象类。语法格式形如:abstract class 抽象类名 类体抽象类只是具体实体的抽象描述,是不可以实例化对象。尽管抽象类定义构造方法是没有语法错误,但是没有实际意义。抽象类中可以定义非抽象方法,也可以定义抽象方法。抽象方法在它的派生子类中可以覆盖,来实现具体功能。抽象类派
13、生出的子类,必须对抽象类中的抽象方法重写。如果没有对抽象方法重写,该子类须定义为抽象类,否则产生编译错误。与抽象方法类似,如果一个类要定义为抽象类,就不能用关键字 static 或关键字 final修饰,原因同抽象方法,在这里就不再说明。例 5.2 抽象类的示例。public abstract class AbstractClassExample /AbstractClassExample.javapublic abstract void showMessage(); /定义抽象方法 showMessage()public void printClassName(String name)/定义
14、非抽象方法 printClassName(String)System.out.println(name);public class DerivedAbstractClass extends AbstractClassExample/DerivedAbstractClass.javapublic DerivedAbstractClass()System.out.println(“Derived Abstract Class Definition“);public void showMessage() /覆盖父类的 showMessage()方法;System.out.println(“Deriv
15、ed Abstract Class Message“);public static void main(String args)DerivedAbstractClass dac=new DerivedAbstractClass();dac.showMessage();dac.printClassName(“Class name is: DerivedAbstractClass“);/引用继承来的方法System.exit(0);图 5-4 例 5.2 的运行结果5.3 多态性多态性是面向对象程序设计的重要特性,它是继承机制产生的结果。所以,继承是多态的前提。面向对象程序设计中,严格的来说多态性
16、是运行绑定机制。这种机制是实现将方法名绑定到方法具体实现代码。通俗地理解就是“一个名字,多种形式” 。实际上,最常见的多态的是符号“+” ,有如下的表示式:6+5 /实现整数相加3+5.0f /将 3 隐性转化为 float 类型,实现单精度数字的相加“IAMCHINESE“+4 /实现字符串连接操作。 同一个“+”有多种含义:整数相加、单精度数字相加、双精度数字相加、字符串连接等操作。当然,上述的表达式 3+5.0f 中数据类型的隐性的转换也是一种多态的体现。又如,在第三章讨论的对象的 finalize()方法也是多态。因为,不同类型的对象都有 finalize()方法,但根据要求的不同,可
17、以赋予 finalize()方法不同代码内容,产生不同的功能。另外,类的构造方法也是多态的一种形式。在创建对象时,根据参数的性质来选择构造方法初始化对象。5根据消息选择响应方法的角度来看,多态分成两种形式:编译多态(Compile-time Polymorphism)和运行多态(Run-time Polymorphism) 。在编译时期根据信息选择响应的方法称为编译多态。Java 语言中,实现编译多态性主要有方法的重载。运行的多态是在程序运行的时才可以确认响应的方法。通常运行的多态是通过继承机制引起的。从实际编程的角度来看,Java 语言实现多态性有三种形式: 1)方法的重载;2)通过继承机制
18、而产生的方法覆盖;3)通过接口实现的方法覆盖(见 5.4.2) 。在本节中对方法的重载和方法的覆盖做一个详细的介绍。5.3.1 方法的重载重载(Overloading)实质上就是在一个类内用一个标识符定义不同的方法或符号运算的方法名或符号名。Java 语言中支持符号的重载,如前提及的运算符 “+”。不过 Java 语言中不支持用户自定义的符号重载,可以支持用户定义方法的重载。方法的重载具体可以理解为,同一个方法名对应不同的方法定义。这些方法的格式说明中的参数不同,即,具体涉及到参数的类型、参数的个数是有所不同的。值得注意的是,在 Java 语言中,方法的返回值和方法的访问控制不作为区分方法的一
19、个因素。请看下列的方法定义格式:public void showMessage()public void showMessage(int x)/与 不同的参数 1public void showMessage(int y)/ 与 不同的参数名 2private void showMessage()/与 不同的访问控制 1public int showMessage()/与 不同的返回值类型 1在这些表达式中方法定义和方法定义是可以视为方法的重载。方法定义 和方法 1定义 和方法定义 不是方法的重载。在编译时会将方法定义 、方法定义 和方法定义 4 5 1 4作为同一种方法,如果这三种形式的 s
20、howMessage()方法放在同一个类中,会产生编译错 5误。尽管方法定义 和方法定义 具有不同的参数名,但二者是同一个方法定义,不能视 2 3之为重载,因为参数个数和参数类型相同。例 5.3 Java 中方法重载的示例。public class OverloadingExamplepublic OverloadingExample() /无参构造方法System.out.println(“方法重载示例:无参构造方法“);public OverloadingExample(String string)/有参构造方法System.out.println(“方法重载示例:有参构造方法,参数:“+
21、string);public void showMessage()System.out.println(“方法重载的信息显示“);public void showMessage(String string)System.out.println(“显示字符串:“+string);public void showMessage(int intValue)System.out.println(“显示整数:“+intValue);public void showMessage(String string,int intValue)System.out.println(“显示字符串:“+string);
22、System.out.println(“显示整数:“+intValue);public static void main(String args)OverloadingExample ole1=new OverloadingExample();OverloadingExample ole2=new OverloadingExample(“对象 2“);ole1.showMessage(1);ole1.showMessage(“对象 1“);ole2.showMessage();ole2.showMessage(“对象 2“);System.exit(0); 图 5-5 例 5.3 的运行结果例
23、 5.3 中,构造方法 OverloadingExample()是重载,因为它有两种形式:有参和无参。在主方法 main()中,根据实参情况不同,由编译器分别调用无参的构造方法和有参的构造方法,创建的对象 ole1 和 ole2。对象 ole1 和对象 ole2 调用成员方法 showMessage()是重载的,根据调用的实际参数的类型,编译器自动加载不同的 showMessage()方法形式。例如,运行ole1 对象的 showMessage(1),编译器将 showMessage 名绑定到方法 showMessage(int)定义的代码中,具体的运行结果观察图 5-5。在下面通过一个实际问
24、题来讨论重载。例 5.4 定义一个类 Counter,具有实现求绝对值运算的功能。public class Counterpublic Counter()System.out.println(“求绝对值“);public int abs(int x) /整数求绝对值return x=0?x:-x;public long abs(long x) /长整数求绝对值return x=0?x:-x;public float abs(float x) /单精度求绝对值return x=0?x:-x;public double abs(double x) /双精度求绝对值return x=0?x:-x;p
25、ublic static void main(String args)Counter c=new Counter();System.out.println(“-30.445 的绝对值=“+c.abs(-30.445);System.out.println(“-30 的绝对值=“+c.abs(-30);System.exit(0);7图 5-6 例 5.4 的运行结果5.3.2 方法的覆盖和隐藏方法的覆盖(Overriding)实质是指子类具有重新定义父类成员方法的能力。这种重新定义表示子类定义的方法具有和父类的方法同名称、同参数类型、同参数个数、以及同返回值。方法格式定义尽管相同,但具有不同的
26、方法体,实现的内容根据程序员的要求而有所不同。子类的方法覆盖父类的同名方法。这意味着,子类对象的实例方法调用只会调用子类中定义的方法,而不是父类中同格式说明的方法。从第 4 章介绍可以知道,类的静态方法是一种类方法,对该类的所有对象是共享的。有一种特殊的情况,就是子类覆盖了父类内定义的静态方法。这时,父类的静态方法被隐藏起来了,在子类中失去作用。要求子类定义的方法必须和父类的静态方法具有相同性质和相同的方法格式说明。即,同为静态类型、同方法名、同方法参数类型、同方法参数个数、同返回值。如果子类定义的方法在重新定义父类的静态方法时,没有用关键字 static 定义,则会产生错误。因为,用一句话可
27、以概括成:如果子类的方法覆盖父类的静态方法,则父类的方法在子类中隐藏起来了。为了说明覆盖的定义,请看例 5.5。例 5.5 方法覆盖和隐藏的示例。public class ParentClass /ParentClass.javapublic static String getMessage() /ParentClass 类方法 getMessage();return “获取 ParentClass 类的对象的信息 “;public void showMessage(String message) /被 ChildClass 的 showMessage()方法隐藏System.out.prin
28、tln(“输出 ParentClass 类的对象的信息:“+message);public class ChildClass extends ParentClass /ChildClass.javapublic static String getMessage() /定义 ChildClass 类方法 getMessage();return “获取 ChildClass 类的对象的信息“;public void showMessage(String message) /覆盖了 ParentClass 的 showMessage()方法。System.out.println(“输出 ChildC
29、lass 类的对象的信息:“+message);public class OverridingTest /定义测试类 OverridingTestpublic static void main(String args)ParentClass cc=new ChildClass(); /对象变量 cc 引用 ChildClass 的对象System.out.println(“输出一个对象“);cc.showMessage(“Writen by Chen“); /调用对象 cc 的方法 showMessage()System.out.println(cc.getMessage(); /输出对象 c
30、c 的获取信息System.out.println(“输出另外一个对象“);cc=new ParentClass(); /对象变量 cc 引用 ParentClass 类的对象cc.showMessage(“Writen by Chen“); System.out.println(cc.getMessage(); System.out.println(“再输出一个 ChildClass 的对象“); ChildClass cc2=new ChildClass(); /创建对象 cc2cc2.showMessage(“Writen by Chen“);System.out.println(cc2
31、.getMessage(); System.exit(0);图 5-7 图 5.5 的运行结果从图 5-7 中显示的运行结果可以观察到两方面的内容。首先,子类 ChildClass 的showMessage()方法覆盖了父类 ParentClass 的同名方法。在子类 ChildClass 中定义getMessage()覆盖了父类的类方法 getMessage(),而父类的类方法 getMessage()在子类中隐藏,没有发挥作用。如果在子类定义方法 getMesssage()中没有用关键字 static 修饰,这时会产生编译错误。其次,在上例中,对象变量 cc 声明为 ParentClass
32、 的对象,但是该对象变量即可引用 ParentClass 的对象,也可以引用 ChildClass 对象。具体引用对象的类型,取决于在运行期间该对象变量引用的对象的类型,这就是运行时的多态。子类覆盖父类的方法,访问控制可以不同。但是,子类的访问控制的访问权限不能低于父类的同名方法访问权限。否则,会产生编译错误。例如,将例 5.5 中的 ParentClass.java和 ChildClass.java 改成如下形式:public class ParentClasspublic static String getMessage()return “Hello, Parent Class“;prot
33、ected void showMessage(String message) /保护类型System.out.println(“Parent Class Message “+message);public class ChildClass extends ParentClassprivate static String getMessage() /定义私有类型,出现编译错误,要修改成 public 则正确。return “Hello Child Class“;public void showMessage(String message)/正确System.out.println(“Child
34、Class Message “+message);从编译上述程序,子类覆盖父类方法 protected static String getMessage(),出现编译错误,要修改成 protected 或 public 则正确。5.3.3 数据成员的隐藏父类的数据成员可以在子类中隐藏,只要子类中定义了同名的数据成员。即使数据成员的类型不同,也视为父类的数据成员被隐藏了。如果要在子类中引用父类的同名数据成9员,只能通过关键字 super(见 5.3.4)来实现。例 5.6 数据成员隐藏的示例。public class ParentClass /ParentClass.javapublic Str
35、ing string; /定义 string 为 Stringpublic ParentClass()string=“Parent Class“;System.out.println(“Parent Class Definition: “+string);protected static String getMessage()return “Hello, Parent Class“;protected void showMessage(String message)System.out.println(“Parent Class Message “+message);public class
36、ChildClass extends ParentClass /ChildClass.javapublic int string; /定义 string 为 intpublic ChildClass()string=0;System.out.println(“Child Class Definition: “+string);protected static String getMessage()return “Hello Child Class“;public void showMessage(String message)System.out.println(“Child Class Me
37、ssage “+message);public class OverridingTest /OverridingTest.javapublic static void main(String args)ChildClass cc=new ChildClass();cc.showMessage(“Writen by Chen“);System.out.println(cc.getMessage();System.exit(0);图 5-8 例 5.6 的运行结果5.3.4 super 关键字super 关键字表示对类的父类的引用。在子类中有时会使用父类的数据和方法,这时就可以利用 super 关
38、键字来实现对父类的引用。super 关键字不是一个对象的引用,而是调用父类的成员特有的关键字。super 关键字主要应用在两个方面: 1)应用于引用父类的构造方法;2)应用于引用父类中被子类覆盖的成员方法和隐藏数据成员。例 5.7 super 关键字的示例。public class SuperClass /SuperClass.javaprivate String string;private int intValue;public SuperClass() /构造方法string=“Super Class“;intValue=1;public void setIntValue(int x)i
39、ntValue=x;public void setStringValue(String string)this.string=string;public int getIntValue()return intValue;public String getStringValue()return string;public void showMessage()System.out.println(“父类的信息有:“+string+“,“+intValue);public class SubClass extends SuperClass /SubClass.javapublic String st
40、ring;public int intValue;public char subChar;public SubClass()super(); /引用父类的构造方法subChar=C;public void setChar(char c)subChar=c;public char getChar()return subChar;public void showMessage()super.showMessage(); /引用父类的被覆盖的方法 showMessage()System.out.println(“子类增加信息:“+subChar);public class SuperTest /Su
41、perTest.javapublic static void main(String args)SubClass sc=new SubClass(); /创建对象 scsc.showMessage(); /调用方法 showMessage()System.exit(0);11图 5-9 例 5.7 的运行结果值得注意的是,父类的私有数据成员不能通过 super 关键字来访问。因为父类的私有数据成员的作用域只在定义类中有效。所以,在 SubClass.java 中试图通过 super.string 来访问父类定义的数据 string,会产生编译错误。另外,不能通过 super 关键字在子类对象中
42、引用父类静态成员。因为,类定义的静态成员方法和静态数据成员被加载后会占据固定的存储空间,对所有类的对象有效。子类静态成员占据的空间与父类静态成员占据的空间没有关系。如果在子类引用父类的静态类方法和静态数据成员会产生编译错误。把例 5.7 改写成如下形式后 SubClass.java 会出现编译错误。public class SuperClassprivate static String string; /定义静态数据 string private static int intValue; /定义静态数据 intpublic SuperClass()string=“Super Class“;in
43、tValue=1;public void setIntValue(int x)intValue=x;public void setStringValue(String string)this.string=string;public int getIntValue()return intValue;public String getStringValue()return string;public static void showMessage() /定义静态成员方法System.out.println(“父类的信息有:“+string+“,“+intValue);public class S
44、ubClass extends SuperClasspublic static String string;public static int intValue;public char subChar;public SubClass()super();subChar=C;public void setChar(char c)subChar=c;public char getChar()return subChar;public static void showMessage()super.showMessage(); /编译错误System.out.println(“子类增加信息:“+subC
45、har);5.4 接口Java 语言不支持多继承,但是在有的情况下需要使用多继承。比如大学的人员分成学生和在校的职工。如果某些在校职工正在读本校的在职研究生,这时他们就具有在校职工和学生两种身份。用 UML 简图来表示,如图 5-10 所示,其中,用 Person 表示人,用Student 表示学生,用 Employee 表示职工,用 EmployedStudent 表示在读的本校职工。在现实生活中类似的多继承的例子随处可见。图 5-10 UML 简图表示多继承可是,多继承的存在有可能导致“二义性问题” 。假设 Person 类具有某个公共方法如setName(),那么类 Employee 和
46、类 Student 都继承了该公共方法,在这两个类中通过覆盖可以重写该方法 setName(),当在类 EmployedStudent 的对象中调用实例方法 setName()时,将无法判别调用的是从类 Employee 继承而来的方法,还是从类 Student 中继承的方法,具体的调用完全取决于运行状态。这种“二义性问题”会导致用户对程序代码的错误的理解。Java 语言可以通过接口概念,一方面实现多继承,同时避免 “二义性问题”的产生,另一方面为 Java 语言在支持单继承机制的前提下实现多态提供了方便。所谓的接口(Interface )实质上就是为外界提供运算,而不揭示这些运算的结构或具体
47、的实现内容。从编程的角度看,Java 语言定义的接口实际上是一组抽象方法和常量的集合,为其他类提供运算的外部说明。接口为一个或多个类提供一个行为规范,具体的实现在这些类中完成,这些类之间并不存在层次关系。通过接口可以了解对象的实例方法的原型,而不需要了解这些方法的具体实现。接口的存在有效解决了如何实现多继承,同时避免了由于多继承产生的“二义性问题” 。5.4.1 接口的格式定义Java 语言中,接口是通过关键字 interface 来实现定义的,具体格式如下:public interface 接口名 extends 父接口名表接口体接口名的定义规则同类名的规则。接口名可以由多个单词组成,每个单
48、词的首字母为大写。接口可以有继承关系,通过关键字 extends 实现继承。接口体中定义的是方法和常量。接口体中定义的所有方法不管是否用来显式的定义为公共的抽象性质(public abstract) ,还是缺省性质定义,所有方法隐含为公共的(public)和抽象(abstract)的,方法体为空。接口体中定义的常量格式如下:final static数据类型 常量标识符= 常量值;EmployeeEmployedStudentStudentPerson13例 5.8 接口的示例。/Movable.javainterface Movable /定义接口 Movablepublic abstract void move();/Drawable.javainterface Drawable /定义接口 Drawablepublic ab