第5章继承与多态 知识要点: 1. 继承 2. 引用类型的转换 3. 多态 4.final修饰符 5.Object类 学习目标: 继承和多态是面向对象编程的两大核心特征。通过继承可以更有效地组织程序结构, 最大限度地达到代码的重复利用和优化;通过多态使用同样的方法调用形式,却可以实现不 一样的行为和结果。 通过本章的学习,读者可以理解继承的概念;掌握子类的定义、子类对象的创建过程和 继承关系中的内存分配;掌握方法重写与方法重载的应用以及两者的区别;理解引用类型转 换中的上转型和下转型;掌握多态的两种形式;掌握final修饰符的用法;理解Object类。 5.继承 1 5.1 继承概述继承 1. Java语言中使用类描述现实世界中的事物。类源自分类学的概念,就像生物可以分为 植物和动物,动物又可以分为人类和猫类等。分类层次图如图5-1所示。 图5-1 分类层次图 下面以“人类”为例讲解继承关系的必要性。假设“人类”按照职业可以分为“教师”“学 生”和“职员”等,没有继承关系时,为了描述“教师”这一类事物时,会抽象出教师编号、名字、 年龄、身高、职称等基本特征,以及说话、教学等行为特征;为了描述“学生”这一类事物时,会 抽象出学号、名字、年龄、身高等基本特征以及说话、学习等行为特征。这两类事物中有很多 相同或者相似的基本特征以及行为特征,是否有一种方式或者机制,可把多个不同类事物之 间相同的部分提取出来,共同管理? 这就是继承由来的原因。 ·125· 读者会发现,“教师”和“学生”都是“人类”的1种,“人类”可以拥有所有人具有的基本特 征(如名字、年龄和身高)和行为(如说话),而“教师”继承自“人类”,自动拥有“人类”的特征 和行为,并且还可以拥有“教师”独特的特征(如教师编号和职称)和行为(如教学),“学生”继 承自“人类”,也会自动拥有“人类”的特征和行为,并且可以拥有“学生”独特的特征(如学号) 和行为(如学习)等。 通常把分类层次中处于上层(大类)的类称为父类或者超类,把处于下层并由该父类派 生出的小类称为子类或者派生类。在整个类继承层次中处于最顶层的类是Object类,它是 所有类的父类,也称为根类。除了Object类之外,所有的类都有父类。 继承是面向对象程序设计中的重要机制之一,它使编程人员可以在原有类的基础上快 速设计出功能更强大的新类,而不必从头开始定义,避免了很多重复性的工作。在继承关系 中,子类会自动拥有父类的属性和方法,同时也可以加入自己的一些特性,使得子类更具体, 功能更丰富。 继承分为单继承和多继承两种方式。在单继承中,每一个类只能有一个父类(如人的亲 生父亲只能有一个),而多继承则每一个类可以有多个父类。单继承是最常见的继承方式, 因为其条理清晰,语法规则简单,更接近现实世界,所以Java语言中采用的是单继承方式。 5.1.2 子类的继承规则 在子类定义中,使用关键字extends表示继承关系,格式如下: [修饰符] class 子类名 extends 父类名{ 子类体 } 例如: public class Person [extends Object]{父类体} public class Student extends Person{子类体} 说明: ① 修饰符,通常是访问权限修饰符public和默认修饰符。 ② 需要先创建父类,之后才可以创建子类。子类名和父类名都需要满足标识符规则。 ③ Student类是Person类的子类,Person类是Student类的父类。 ④ 每个类都有父类,如果类在定义时没有使用extends关键字显式声明父类,那么系统 默认其父类是Object类,中括号[]部分为可选。 子类可以继承父类中除了构造方法以外的所有属性和方法,同时也可以在父类的基础 上增加新的属性或者新的方法。 【例5-1】 继承关系的使用。 Person类 1 public class Person { 2 String name="zhangsan"; 3 int age=18; 4 double height=1.73; ·126· 5 public void say() { 6 System.out.println("Person 类中的say()方法"); 7 } 8 } Student类 1 public class Student extends Person { 2 String sno="s01"; 3 public void study() { 4 System.out.println("Student 类中的study()方法"); 5 } 6 } Test类 1 public class Test { 2 public static void main(String[] args) { 3 Student s1=new Student(); 4 System.out.println(s1.name+" "+s1.age+" "+s1.height+" "+s1.sno); 5 s1.say(); 6 s1.study(); 7 } 8 } 运行结果: zhangsan 18 1.73 s01 Person 类中的say()方法 Student 类中的study()方法 代码解释: 图5-2 继承关系示意图 第1行Person类后省略了extendsObject语句,为了简化代 码的调用,在第2~4直接给3个实例变量赋初值。 Student类是Person类的子类,在Student类中定义了实例 变量sno并对其赋初值,以及定义了study()方法。 Test类中第3行创建了Student对象s1,第4行使用对象名 s1访问了从Person类中继承过来的3个实例变量以及子类中定 义的sno,第5行调用Person类中继承过来的实例方法say(),第 6行调用子类中定义的实例方法study()。 例5-1中UML所绘制的继承关系示意图如图5-2所示。 说明:在图5-2中空心箭头表示继承关系(又被称为泛化关 系),箭头由子类指向父类。 5.1.3 子类对象的创建和super 在创建子类对象的过程中,首先会调用子类的构造方法,但是在子类构造方法中总会默 ·127· 认调用父类的构造方法,先完成父类实例变量的初始化,再完成子类实例变量的初始化,之 后才可以创建出子类对象。 子类调用父类构造方法的规则如下: super([参数列表]); 说明: ①super()必须在子类构造方法的第1行使用,注释语句除外。 ② 参数列表由父类构造方法的参数决定。 当例5-1中的Person类换成如下代码时,Student类会出现编译错误。 1 public class Person { 2 String name="zhangsan"; 3 int age=18; 4 double height=1.73; 5 public Person(String name) { 6 this.name=name; 7 } 8 public void say() { 9 System.out.println("Person 类中say()方法"); 10 } 11 } Student类会出现的错误信息:“在默认构造方法中无法调用Person类中的Person() 空构造方法”。 按照构造方法的特点,每个类中都有构造方法,当类中没有定义任何构造方法时,系统 会为该类中增加一个空构造方法,而在子类的构造方法的第1行中,总会使用super()调用 父类的空构造方法。 在Student类中补充完整的代码如下: 1 public class Student extends Person { 2 String sno="s01"; 3 public Student() { 4 super(); 5 } 6 public void study() { 7 System.out.println("Student 类中的study()方法"); 8 } 9 } 代码解释: 第3~5行为系统按照类创建的规则自动添加的内容,这部分往往是初学者容易忽略的 地方。第4行调用Person类中的空构造方法,因为父类中有带参数的构造方法,所以系统 就不会为Person类提供空构造方法,调用失败,出现编译错误。 该类问题的解决办法是在父类中增加一个空构造方法,如下列所示。 ·128· 【例5-2】 在Person类中增加空构造方法。 1 public class Person { 2 String name="zhangsan"; 3 int age=18; 4 double height=1.73; 5 public Person() {} 6 public Person(String name) { 7 this.name=name; 8 } 9 public void say() { 10 System.out.println("Person 类中say()方法"); 11 } 12 } 说明:第5、6行Person类中的2个构造方法是方法重载关系。 5.1.4 继承关系中的内存分配 当创建子类对象时,如何为子类对象分配内存空间? 子类对象的内存分配主要分为栈 内存、堆内存和方法区3部分。栈内存用来保存对象名,即该对象在堆内存的首地址。堆内 存用来保存子类对象的实例变量,以及从父类继承过来的实例变量,同时在子类对象里保留 对方法区中实例方法区和类方法区的引用。 例5-1子类对象内存分配示意图如图5-3所示。 图5-3 例5-1子类对象内存分配示意图 说明:this是堆内存中的引用,为了访问当前对象。sno为Student类中定义的实例变 量,name、age、height为从Person类继承过来的实例变量。另外,因为Person类和Student 类中只有实例方法,所以本图中只有对实例方法区访问的引用。 ·129· 5.1.5 实例变量的隐藏 实例变量的隐藏是指当子类和父类都有同名的实例变量时,子类的实例变量把父类的 实例变量隐藏起来,子类对象访问时,优先访问子类中定义的实例变量。就像我们将一张大 小完全相同的扑克牌放在另外一张扑克牌前一样,这两张扑克牌都是真实存在的,前一张扑 克牌把后一张扑克牌隐藏起来。初学者很容易将其错误地理解为替代或覆盖关系。 为了能够访问被子类实例变量隐藏的父类中的实例变量,需要使用“super.实例变量 名”。“super.”表示对父类的引用,和“this.”表示对子类的引用应区别理解。 【例5-3】 实例变量的隐藏。 Person类 1 public class Person { 2 String name="zhangsan"; 3 int age=18; 4 double height=1.73; 5 } Student类 1 public class Student extends Person { 2 String name="lisi"; 3 String sno="s01"; 4 public void showName() { 5 System.out.println(this.name+" "+super.name); 6 } 7 } Test类 1 public class Test { 2 public static void main(String[] args) { 3 Student s1=new Student(); 4 System.out.println(s1.name); 5 s1.showName(); 6 } 7 } 运行结果: lisi lisi zhangsan 代码解释: 子类Student和父类Person都有同名的实例变量name。在Test类中的第3行创建 Student对象。 第4行访问子类的实例变量name。 第5行调用showName()方法,分别使用this.和super.访问了子类的实例变量和父类 ·130· 的实例变量name。 例5-3对应的内存分配示意图如图5-4所示。 图5-4 父、子类同名实例变量的内存分配示意图 5.1.6 方法重写和方法重载 方法重写(或方法覆盖)是指子类和父类中有同样的实例方法,即方法的访问权限修饰 符、返回数据类型、方法名相同。子类对象调用方法时,优先使用子类自己定义的实例方法, 为了能够访问到父类的实例方法,需要使用“super.实例方法名()”。 子类和父类中的方法也存在方法重载的情况,即方法名相同、参数列表不同。重载方法 的调用由方法名和参数列表决定。 【例5-4】 方法重写和方法重载。 Person类 1 public class Person { 2 String name="zhangsan"; 3 int age=18; 4 double height=1.73; 5 public void say() { 6 System.out.println("Person 类中的say()"); 7 } 8 } Student类 1 public class Student extends Person { 2 String name="lisi"; 3 String sno="s01"; 4 public void say() { 5 super.say(); 6 System.out.println("Student 类中的say()"); ·131· 7 } 8 public void say(String name) { 9 System.out.println(this.name+" say hello to "+name); 10 } 11 } Test类 1 public class Test { 2 public static void main(String[] args) { 3 Student s1=new Student(); 4 s1.say(); 5 s1.say("wangwu"); 6 } 7 } 运行结果: Person 类中的say() Student 类中的say() lisi say hello to wangwu 代码解释: Person类中第5行定义了say()实例方法,Student类中第4行也定义了同样的say() 实例方法,这两个方法是方法重写关系。Student类中第8行的say(Stringname)方法和其 他两个say()方法是方法重载关系。 Test类中第3行创建了子类对象s1,第4行优先调用Student类中的say()方法, Student类中第5行使用super.say()调用了父类的say()方法,并执行输出语句。 Test类中第5行调用了Student类中第8行定义的有参数的say()方法,因为方法参数 是局部变量和类中定义的实例变量重名,所以第9行需要用this.区分,this.name表示实例 变量。注 意:在继承关系中出现的同名方法,要么是方法重写,要么是方法重载,否则会出现 编译错误。 错误的写法,例如: Person类 public String say(String name) { return n; } Student类 public void say(String name) {} 这两个方法既不是方法重写,也不是方法重载,Student类中的方法会出现编译错误。 5.1.7 子类对父类类成员的访问 父类中定义的类成员包括类变量和类方法,对其访问的方式需要使用“父类名.类变量” ·132· 或“父类名.类方法名()”。在类方法中不能使用super.和super(),因为super属于对象的引 用,而类成员属于类的引用。 注意:子类和父类中相同的类方法之间不叫方法重写,类方法的调用只看引用变量的 类型。 【例5-5】 子类对父类类成员的访问。 Person类 1 public class Person { 2 String name="zhangsan"; 3 int age=18; 4 double height=1.73; 5 static String country="china"; 6 public static void show() { 7 System.out.println("Person 类中的类变量:"+country); 8 } 9 } Student类 1 public class Student extends Person { 2 String name="lisi"; 3 String sno="s01"; 4 static String country="中国"; 5 public static void show() { 6 Person.show(); 7 //super.show(); 8 System.out.println("Student 类中的类变量:"+country); 9 } 10 } Test类 1 public class Test { 2 public static void main(String[] args) { 3 Student s1=new Student(); 4 s1.show(); 5 Student.show(); 6 } 7 } 运行结果: Person 类中的类变量:china Student 类中的类变量:中国 Person 类中的类变量:china Student 类中的类变量:中国 代码解释: ·133· Test类中第3行创建了子类Student对象s1,第4行通过“对象名.类方法名()”的方式 调用Student类中定义的类方法show()。 Student类中show()方法调用父类中的类方法show()时,只可以用Person类名调用, 因为在类方法中不可以使用“this.”或者“super.”。第6行调用Person类中的类方法show()。 Test类中第5行使用“类名.类方法名()”的方式调用Student类中的类方法show()。 课堂练习1 1.Java语言中类间的继承关系是( )。 A.多重的B.单重的 C.线程的D.不能继承 2.下列程序的运行结果是( )。 class Parent { void printMe() { System.out.print("parent "); } }c lass Child extends Parent { void printMe() { System.out.print("child "); } void printAll() { super.printMe(); this.printMe(); printMe(); } }p ublic class TestThis { public static void main(String args[]) { Child baby =new Child(); baby.printAll(); } } A.childchildchild B.childparentchild C.parentchildchild D.parentparentchild 3.下列程序的运行结果是( )。 class First { public First() { speak(); } public void speak() { System.out.println("in First class"); ·134·