继承与多态

Posted 老程序员老关

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了继承与多态相关的知识,希望对你有一定的参考价值。

《Java从小白到大牛》纸质版已经上架了!!!

类的继承性是面向对象语言的基本特性,多态性前提是继承性。Java支持继承性和多态性。这一章讨论Java继承性和多态性。

Java中的继承 #java

为了了解继承性,先看这样一个场景:一位面向对象的程序员小赵,在编程过程中需要描述和处理个人信息,于是定义了类Person,如下所示:

//Person.java文件

package com.a51work6;

import java.util.Date;

public class Person 

// 名字

private String name;

// 年龄

private int age;

// 出生日期

private Date birthDate;

public String getInfo() 

return "Person [name=" + name

+ ", age=" + age

+ ", birthDate=" + birthDate + "]";




一周以后,小赵又遇到了新的需求,需要描述和处理学生信息,于是他又定义了一个新的类Student,如下所示:

//Student.java文件

package com.a51work6;

import java.util.Date;

public class Student 

// 所在学校

public String school;

// 名字

private String name;

// 年龄

private int age;

// 出生日期

private Date birthDate;

public String getInfo() 

return "Person [name=" + name

+ ", age=" + age

+ ", birthDate=" + birthDate + "]";




很多人会认为小赵的做法能够理解并相信这是可行的,但问题在于Student和Person两个类的结构太接近了,后者只比前者多了一个属性school,却要重复定义其他所有的内容,实在让人“不甘心”。Java提供了解决类似问题的机制,那就是类的继承,代码如下所示:

//Student.java文件

package com.a51work6;

import java.util.Date;

public class Student extends Person 

// 所在学校

private String school;


Student类继承了Person类中的所有成员变量和方法,从上述代码可以见继承使用的关键字是extends,extends后面的Person是父类。

如果在类的声明中没有使用extends关键字指明其父类,则默认父类为Object类,java.lang.Object类是Java的根类,所有Java类包括数组都直接或间接继承了Object类,在Object类中定义了一些有关面向对象机制的基本方法,如equals()、toString()和finalize()等方法。

提示 一般情况下,一个子类只能继承一个父类,这称为“单继承”,但有的情况下一个子类可以有多个不同的父类,这称为“多重继承”。在Java中,类的继承只能是单继承,而多重继承可以通过实现多个接口实现。也就是说,在Java中,一个类只能继承一个父类,但是可以实现多个接口。

提示 面向对象分析与设计(OOAD)时,会用到UML图1,其中类图非常重要,用来描述系统静态结构。Student继承Person的类图如图12-1所示。类图中的各个元素说明如图12-2所示,类用矩形表示,一般分为上、中、下三个部分,上部分是类名,中部分是成员变量,下部分是成员方法。实线+空心箭头表示继承关系,箭头指向父类,箭头末端是子类。UML类图中还有很多关系,如图12-3所示,如图虚线+空心箭头表示实线关系,箭头指向接口, 箭头末端是实线类。

调用父类构造方法

当子类实例化时,不仅需要初始化子类成员变量,也需要初始化父类成员变量,初始化父类成员变量需要调用父类构造方法,子类使用super关键字调用父类构造方法。

下面看一个示例,现有父类Person和子类Student,它们类图如图12-4所示。

父类Person代码如下:

//Person.java文件

package com.a51work6;

import java.util.Date;

public class Person 

// 名字

private String name;

// 年龄

private int age;

// 出生日期

private Date birthDate;

// 三个参数构造方法

public Person(String name, int age, Date d) 

this.name = name;

this.age = age;

birthDate = d;



public Person(String name, int age) 

// 调用三个参数构造方法

this(name, age, new Date());



...


子类Student代码如下:

//Student.java文件

package com.a51work6;

import java.util.Date;

public class Student extends Person 

// 所在学校

private String school;

public Student(String name, int age, Date d, String school) 

super(name, age, d);this.school = school;



public Student(String name, int age, String school) 

// this.school = school;//编译错误

super(name, age);this.school = school;



public Student(String name, String school)  // 编译错误 ③

// super(name, 30);

this.school = school;




在Student子类代码第①行和第②行是调用父类构造方法,代码第①行super(name, age, d)语句是调用父类的Person(String name, int age, Date d)构造方法,代码第②行super(name, age)语句是调用父类的Person(String name, int age)构造方法。

提示 super语句必须位于子类构造方法的第一行。

代码第③行构造方法由于没有super语句,编译器会试图调用父类默认构造方法(无参数构造方法),但是父类Person并没有默认构造方法,因此会发生编译错误。解决这个编译错误有三种办法:

  1. 在父类Person中添加默认构造方法,子类Student会隐式调用父类的默认构造方法。
  2. 在子类Studen构造方法添加super语句,显式调用父类构造方法,super语句必须是第一条语句。
  3. 在子类Studen构造方法添加this语句,显式调用当前对象其他构造方法,this语句必须是第一条语句。

成员变量隐藏和方法覆盖

子类继承父类后,有子类中有可能声明了与父类一样的成员变量或方法,那么会出现什么情况呢?

成员变量隐藏 #-0

子类成员变量与父类一样,会屏蔽父类中的成员变量,称为“成员变量隐藏”。示例代码如下:

//ParentClass.java文件

package com.a51work6;

class ParentClass 

// x成员变量

int x = 10;

class SubClass extends ParentClass 

// 屏蔽父类x成员变量

int x = 20;public void print() 

// 访问子类对象x成员变量

System.out.println("x = " + x);// 访问父类x成员变量

System.out.println("super.x = " + super.x);


调用代码如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld 

public static void main(String[] args) 

//实例化子类SubClass

SubClass pObj = new SubClass();

//调用子类print方法

pObj.print();




运行结果如下:

x = 20

super.x = 10

上述代码第①行是在ParentClass类声明x成员变量,那么在它的子类SubClass代码第②行也声明了x成员变量,它会屏蔽父类中的x成员变量。那么代码第③行的x是子类中的x成员变量。如果要调用父类中的x成员变量,则需要super关键字,见代码第④行的super.x。

方法的覆盖(Override) #override

如果子类方法完全与父类方法相同,即:相同的方法名、相同的参数列表和相同的返回值,只是方法体不同,这称为子类覆盖(Override)父类方法。

示例代码如下:

//ParentClass.java文件

package com.a51work6;

class ParentClass 

// x成员变量

int x;

protected void setValue()  ①

x = 10;





class SubClass extends ParentClass 

// 屏蔽父类x成员变量

int x;

@Override

public void setValue()  // 覆盖父类方法 ②

// 访问子类对象x成员变量

x = 20;

// 调用父类setValue()方法

super.setValue();



public void print() 

// 访问子类对象x成员变量

System.out.println("x = " + x);

// 访问父类x成员变量

System.out.println("super.x = " + super.x);




调用代码如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld 

public static void main(String[] args) 

//实例化子类SubClass

SubClass pObj = new SubClass();

//调用setValue方法

pObj.setValue();

//调用子类print方法

pObj.print();




运行结果如下:

x = 20

super.x = 10

上述代码第①行是在ParentClass类声明setValue方法,那么在它的子类SubClass代码第②行覆盖父类中的setValue方法,在声明方法时添加@Override注解,@Override注解不是方法覆盖必须的,它只是锦上添花,但添加@Override注解有两个好处:

1. 提高程序的可读性。

2. 编译器检查@Override注解的方法在父类中是否存在,如果不存在则报错。

注意 方法重写时应遵循的原则:

  1. 覆盖后的方法不能比原方法有更严格的访问控制(可以相同)。例如将代码第②行访问控制public修改private,那么会发生编译错误,因为父类原方法是protected。
  2. 覆盖后的方法不能比原方法产生更多的异常。

多态

在面向对象程序设计中多态是一个非常重要的特性,理解多态有利于进行面向对象的分析与设计。

多态概念 #-0

发生多态要有三个前提条件:

  1. 继承。多态发生一定要子类和父类之间。
  2. 覆盖。子类覆盖了父类的方法。
  3. 声明的变量类型是父类类型,但实例则指向子类实例。

下面通过一个示例理解什么多态。如图12-5所示,父类Figure(几何图形)类有一个onDraw(绘图)方法,Figure(几何图形)它有两个子类Ellipse(椭圆形)和Triangle(三角形),Ellipse和Triangle覆盖onDraw方法。Ellipse和Triangle都有onDraw方法,但具体实现的方式不同。

具体代码如下:

//Figure.java文件

package com.a51work6;

public class Figure 

//绘制几何图形方法

public void onDraw() 

System.out.println("绘制Figure...");





//Ellipse.java文件

package com.a51work6;

//几何图形椭圆形

public class Ellipse extends Figure 

//绘制几何图形方法

@Override

public void onDraw() 

System.out.println("绘制椭圆形...");





//Triangle.java文件

package com.a51work6;

//几何图形三角形

public class Triangle extends Figure 

// 绘制几何图形方法

@Override

public void onDraw() 

System.out.println("绘制三角形...");




调用代码如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld 

public static void main(String[] args) 

// f1变量是父类类型,指向父类实例

Figure f1 = new Figure(); ①

f1.onDraw();

//f2变量是父类类型,指向子类实例,发生多态

Figure f2 = new Triangle(); ②

f2.onDraw();

//f3变量是父类类型,指向子类实例,发生多态

Figure f3 = new Ellipse(); ③

f3.onDraw();

//f4变量是子类类型,指向子类实例

Triangle f4 = new Triangle(); ④

f4.onDraw();




上述带代码第②行和第③行是符合多态的三个前提,因此会发生多态。而代码第①行和第④行都不符合,没有发生多态。

运行结果如下:

绘制Figure...

绘制三角形...

绘制椭圆形...

绘制三角形...

从运行结果可知,多态发生时,Java虚拟机运行时根据引用变量指向的实例调用它的方法,而不是根据引用变量的类型调用。

引用类型检查 #-1

有时候需要在运行时判断一个对象是否属于某个引用类型,这时可以使用instanceof运算符,instanceof运算符语法格式如下:

obj instanceof type

其中obj是一个对象,type是引用类型,如果obj对象是type引用类型实例则返回true,否则false。

为了介绍引用类型检查,先看一个示例,如同12-6所示的类图,展示了继承层次树,Person类是根类,Student是Person的直接子类,Worker是Person的直接子类。

继承层次树中具体实现代码如下:

//Person.java文件

package com.a51work6;

public class Person 

String name;

int age;

public Person(String name, int age) 

this.name = name;

this.age = age;



@Override

public String toString() 

return "Person [name=" + name

+ ", age=" + age + "]";





//Worker.java文件

package com.a51work6;

public class Worker extends Person 

String factory;

public Worker(String name, int age, String factory) 

super(name, age);

this.factory = factory;



@Override

public String toString() 

return "Worker [factory=" + factory

+ ", name=" + name

+ ", age=" + age + "]";





//Student.java文件

package com.a51work6;

public class Student extends Person 

String school;

public Student(String name, int age, String school) 

super(name, age);

this.school = school;



@Override

public String toString() 

return "Student [school=" + school

+ ", name=" + name

+ ", age=" + age + "]";




调用代码如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld 

public static void main(String[] args) 

Student student1 = new Student("Tom", 18, "清华大学"); ①

Student student2 = new Student("Ben", 28, "北京大学");

Student student3 = new Student("Tony", 38, "香港大学"); ②

Worker worker1 = new Worker("Tom", 18, "钢厂"); ③

Worker worker2 = new Worker("Ben", 20, "电厂"); ④

Person[] people =  student1, student2, student3, worker1, worker2 ;int studentCount = 0;

int workerCount = 0;

for (Person item : people) if (item instanceof Worker)  ⑦

workerCount++;

 else if (item instanceof Student)  ⑧

studentCount++;





System.out.printf("工人人数:%d,学生人数:%d", workerCount, studentCount);




上述代码第①行和第②行创建了3个Student实例,代码第③行和第④行创建了两个Worker实例,然后程序把这5个实例放入people数组中。

代码第⑥行使用for-each遍历people数组集合,当从people数组中取出元素时,元素类型是People类型,但是实例不知道是哪个子类(Student和Worker)实例。代码第⑦行item instanceof Worker表达式是判断数组中的元素是否是Worker实例;类似地,第⑧行item instanceof Student表达式是判断数组中的元素是否是Student实例。

输出结果如下:

工人人数:2,学生人数:3

引用类型转换 #-2

在5.7节介绍过数值类型相互转换,引用类型可以进行转换,但并不是所有的引用类型都能互相转换,只有属于同一颗继承层次树中的引用类型才可以转换。

在上一节示例上修改HelloWorld.java代码如下:

//HelloWorld.java文件

package com.a51work6;

public class HelloWorld 

public static void main(String[] args) 

Person p1 = new Student("Tom", 18, "清华大学");

Person p2 = new Worker("Tom", 18, "钢厂");

Person p3 = new Person("Tom", 28);

Student p4 = new Student("Ben", 40, "清华大学");

Worker p5 = new Worker("Tony", 28, "钢厂");


上述代码创建了3个实例p1、p2、p3、p4和p5,它们的类型都是Person继承层次树中的引用类型,p1和p4是Student实例,p2和p5是Worker实例,p3是Person实例。首先,对象类型转换一定发生在继承的前提下,p1和p2都声明为Person类型,而实例是由Person子类型实例化的。

表12-1归纳了p1、p2、p3、p4和p5这5个实例与Worker、Student和Person这3种类型之间的转换关系。

表 12-1 类型转换

对 象Person类型Worker类型Student类型说  明
p1支持不支持支持(向下转型)类型:Person实例:Student
p2支持支持(向下转型)不支持类型:Person实例:Worker
p3支持不支持不支持类型:Person实例:Person
p4支持(向上转型)不支持支持类型:Student实例:Student
p5支持(向上转型)支持不支持类型:Worker实例:Worker

作为这段程序的编写者是知道p1本质上是Student实例,但是表面上看是Person类型,编译器也无法推断p1的实例是Person、Student还是Worker。此时可以使用instanceof操作符来判断它是哪一类的实例。

引用类型转换也是通过小括号运算符实现,类型转换有两个方向:将父类引用类

以上是关于继承与多态的主要内容,如果未能解决你的问题,请参考以下文章

C++--第17课 - 继承与多态 - 上

多态与继承_16

JAVA实验4 类与对象(封装继承多态等机制的使用)

Java面向对象:多态特性的学习

面向对象编程三大特性-封装、继承、多态

实验三 类的继承和多态性