在 Java 的面向对象程序设计中,继承是不可或缺的一部分。通过继承可以实现代码的复用,提高程序的可维护性。
在现实世界中的对象存在很多如下图的关系:
图 1 不同车之间的关系
巴士、卡车和出租车都是汽车的一种,分别拥有相似的特性,如对所有的交通工具而言都具备引擎数量、外观的颜色,相似的行为如刹车和加速的功能。但每种不同的交通工具又有自己的特性,例如:
巴士拥有和其他交通工具不同的特性和行为,即最大载客数量和到指定站点要报站的特点;
卡车的主要功能是运送货物,也就是载货和卸货,因此拥有最大载重的特性。
面向对象的程序设计中该怎样描述现实世界中的这种情况呢?这就要用到继承的概念。继承就是从已有的类派生出新的类。新的类能吸收已有类的数据属性和行为,并能扩展新的能力。
已有的类一般称为父类(基类或超类)。由基类产生的新类称为派生类或子类。派生类同样也可以作为基类再派生新的子类,这样就形成了类间的层次结构。修改后的交通工具间的继承关系如下图所示。
图 2 继承关系
汽车被抽象为父类(基类或超类),代表一般化属性。而巴士、卡车和出租车转化为子类,继承父类的一般特性包括父类的数据成员和行为,如外观颜色和刹车等特性,又产生自己独特的属性和行为,如巴士的最大载客数和报站。
继承的方式包括单一继承和多重继承:
单一继承是最简单的方式,一个派生类只从一个基类派生;
多重继承是一个派生类有两个或多个基类。
两种继承方式如下图所示:
图 3 继承的方式
图中箭头的方向表示继承的方向,由子类指向父类。
通过上面介绍可以看出基类与派生类的关系:
基类是派生类的抽象(基类抽象了派生类的公共特性);
派生类是对基类的扩展;
派生类和基类的关系相当于“是一个(is a)”的关系,即派生类是基类的一个对象,而不是“有(has)”的组合关系,即类的对象包含一个或多个其他类的对象作为该类的属性,如汽车类拥有发动机、轮胎,这种关系称为类的组合。
注意,Java 语言只支持单一继承,不支持多重继承。
例如设计并实现教师类,其中教师分为 Java 教师以及 .NET 教师,各自的要求如下:
Java教师:
属性:姓名、所属部门
方法:授课(打开 Eclipse、实施理论课授课)、自我介绍
.NET教师:
属性:姓名、所属部门
方法:授课(打开 Visual studio 2010、实施理论课授课)、自我介绍
根据要求我们分别定义 Java 教师类和 .NET 教师类,代码如下:
public class JavaTeacher {
private String name; // 教师姓名
private String school; // 所在学校
public JavaTeacher(String myName, String mySchool) {
name = myName;
school = mySchool;
}
public void giveLesson() { // 授课方法的具体实现
System.out.println("启动 MyEclipse");
System.out.println("知识点讲解 ");
System.out.println("总结提问 ");
}
public void introduction() { // 自我介绍方法的具体实现
System.out.println("大家好!我是" + school + "的" + name + ".");
}
}
public class DotNetTeacher {
private String name; // 教师姓名
private String school; // 所在学校
public DotNetTeacher(String myName, String mySchool) {
name = myName;
school = mySchool;
}
public void giveLesson() {
System.out.println("启动 VS2010");
System.out.println("知识点讲解 ");
System.out.println("总结提问 ");
}
public void introduction() {
System.out.println("大家好!我是" + school + "的" + name + ".");
}
}
通过以上代码可以看到,JavaTeacher 类和 DotNetTeacher 类有很多相同的属性和方法,例如都有姓名、所在学校属性,都具有授课、上课功能。在实际开发中,一个系统中往往有很多类并且它们之间有很多相似之处,如果每个类都将这些相同的变量和方法定义一遍,不仅代码乱,工作量也很大。
在这个例子中,可以将 JavaTeacher 类和 DotNetTeacher 类的共同点抽取出来,形成一个 Teacher 类,代码如下:
public class Teacher {
private String name; // 教师姓名
private String school; // 所在学校
public Teacher(String myName, String mySchool) {
name = myName;
school = mySchool;
}
public void giveLesson() { // 授课方法的具体实现
System.out.println("知识点讲解");
System.out.println("总结提问");
}
public void introduction() { // 自我介绍方法的具体实现
System.out.println("大家好!我是" + school + "的" + name + "。");
}
}
然后让 JavaTeacher 类和 DotNetTeacher 类继承 Teacher 类,在 JavaTeacher 类和 DotNetTeacher 类中可以直接使用 Teacher 类中的属性和方法。
Java 中,子类继承父类的语法格式如下:
【修饰符】class 子类名 extends 父类名 {
// 子类的属性和方法的定义
};
修饰符:可选,用于指定类的访问权限,可选值 public、abstract 和 final。
class 子类名:必选,用于指定子类的名称。
extends 父类名:必选,用于指定要定义的子类继承于哪个父类。
使用继承实现以上代码【实例 1】:
class Teacher {
String name; // 教师姓名
String school; // 所在学校
public Teacher(String myName, String mySchool) {
name = myName;
school = mySchool;
}
public void giveLesson() { // 授课方法的具体实现
System.out.println(" 知识点讲解 ");
System.out.println(" 总结提问 ");
}
public void introduction() { // 自我介绍方法的具体实现
System.out.println("大家好!我是 " + school + " 的 " + name + "。");
}
}
class JavaTeacher extends Teacher {
public JavaTeacher(String myName, String mySchool) {
super(myName, mySchool);
}
public void giveLesson() {
System.out.println("启动 Eclipse");
super.giveLesson();
}
}
class DotNetTeacher extends Teacher {
public DotNetTeacher(String myName, String mySchool) {
super(myName, mySchool);
}
public void giveLesson() {
System.out.println("启动 VS2010");
super.giveLesson();
}
}
public class TestTeacher {
public static void main(String args[]) {
// 创建 javaTeacher 对象
JavaTeacher javaTeacher = new JavaTeacher("张三", "xxx大学");
javaTeacher.introduction();
javaTeacher.giveLesson();
System.out.println("\n");
// 创建 dotNetTeacher 对象
DotNetTeacher dotNetTeacher = new DotNetTeacher("李四", "xxx学院");
dotNetTeacher.introduction();
dotNetTeacher.giveLesson();
}
}
程序运行结果为:
大家好!我是 xxx大学 的 张三。
启动 Eclipse
知识点讲解
总结提问
大家好!我是 xxx学院 的 李四。
启动 VS2010
知识点讲解
总结提问
通过关键字 extends 分别创建父类 Teacher 的子类 JavaTeacher 和 DotNetTeacher。子类继承父类所有的成员变量和成员方法,但不能继承父类的构造方法。在子类的构造方法中可使用语句 super(参数列表) 调用父类的构造方法。
TestTeacher 的 main() 方法中声明两个子类对象。子类对象分别调用各自的方法进行授课和自我介绍。如语句 javaTeacher.giveLesson(),就调用 JavaTeacher 子类的方法实现授课的处理,该子类的方法来自对父类 Teacher 方法 giveLesson() 的继承,语句 super.giveLesson() 代表对父类同名方法的调用。
Java继承的使用原则
1) 方法覆盖
在继承关系中,子类从父类中继承可访问的方法。但有时从父类继承的方法不能完全满足子类需要,这时就需要在子类的方法里修改父类的方法,即子类重新定义从父类继承的成员方法,这个过程称为方法覆盖或重写。
在实例 1 中,父类 Teacher 中定义了 giveLesson() 方法,但是两个子类也各自定义了自己的 giveLesson() 方法。
在进行方法覆盖时,特别需要注意,子类在覆盖父类方法时应注意以下几点:
子类的方法不能缩小父类方法的访问权限;
父类的静态方法不能被子类覆盖为非静态方法;
父类的私有方法不能被子类覆盖;
父类的 final 不能被覆盖。
另外,需要注意方法重载与方法覆盖的区别:
第一,方法重载是在同一个类中,方法重写是在子类与父类中。
第二,方法重载要求方法名相同,参数列表不同;方法覆盖要求子类与父类的方法名、返回值和参数列表相同。
第三,方法重载解决了同一个类中,相同功能的方法名称不同的问题;方法覆盖解决子类继承父类之后,父类的某一个方法不满足子类的具体要求,此时需要重新在子类中定义该方法。
2) 成员变量覆盖
子类也可以覆盖继承的成员变量,只要子类中定义的成员变量和父类中的成员变量同名,子类就覆盖继承的成员变量。
总之,子类可以继承父类中所有可被子类访问的成员变量和成员方法,但必须遵循以下原则:
父类中声明为 public 和 protected 的成员变量和方法可以被子类继承,但声明为 private 的成员变量和方法不能被子类继承;
如果子类和父类位于同一个包中,则父类中由默认修饰符修饰的成员变量和方法可被子类继承;
子类不能继承父类中被覆盖的成员变量;
子类不能继承父类中被覆盖的成员方法。
【实例 2】定义一个动物类 Animal,包含两个成员变量 live 和 skin 以及两个成员方法 eat() 和 move();再定义 Animal 的子类 Bird,在该类中隐藏父类的成员变量 skin,覆盖父类的 move() 方法,并定义测试类进行测试。
class Animal {
public boolean live = true;
public String skin = "";
public void eat() {
System.out.println("动物需要吃食物");
}
public void move() {
System.out.println("动物会运动");
}
}
class Bird extends Animal {
public String skin = "羽毛";
public void move() {
System.out.println("鸟会飞翔");
}
}
public class Main {
public static void main(String[] args) {
Bird bird = new Bird();
bird.eat();
bird.move();
System.out.println("鸟有: " + bird.skin);
}
}
eat() 方法是从父类 Animal 继承下来的方法,move() 方法是 Bird 子类覆盖父类的成员方法,skin 变量为子类自己定义的成员变量。
程序运行结果为:
动物需要吃食物
鸟会飞翔
鸟有: 羽毛
Java继承的传递性
Java 语言虽然不支持多重继承,但支持多层继承,即一个类的父类可以继承另外的类,这称为类继承的传递性。
类的传递性对 Java 语言有重要的意义。如下代码演示了继承的传递性,定义了三个类 Vehicle、Trunk、SmallTruck,其中类 Trunk 继承 Vehicle,类 SmallTruck 继承 Trunk,并测试 SmallTruck 可以继承 Vehicle 的成员。
public class Vehicle {
void vehicleRun() {
System.out.println("汽车在行驶!");
}
}
public class Truck extends Vehicle { // 直接父类为 Vehicle
void truckRun() {
System.out.println("卡车在行驶!");
}
}
public class SmallTruck extends Truck { // 直接父类为 Truck
protected void smallTruckRun() {
System.out.println("微型卡车在行驶!");
}
public static void main(String[] args) {
SmallTruck smalltruck = new SmallTruck();
smalltruck.vehicleRun(); // 祖父类的方法调用
smalltruck.truckRun(); // 直接父类的方法调用
smalltruck.smallTruckRun(); // 子类自身的方法调用
}
}
程序中,SmallTruck 继承了 Truck,Truck 继承了 Vehicle,所以 SmallTruck 同时拥有 Truck 和 Vehicle 的所有可以被继承的成员。
从本例可以看出,Java 语言的继承关系既解决了代码复用的问题,又表示了一个体系,这是面向对象中继承真正的作用。运行该程序,执行结果为:
汽车在行驶!
卡车在行驶!
微型卡车在行驶!
Java super关键字
super 关键字主要用于在继承关系中实现子类对父类方法的调用,包括对父类构造方法和一般方法的调用。
1) 调用父类的构造方法
子类可以调用父类的构造方法,但是必须在子类的构造方法中使用super关键字调用,并且必须把super放在构造方法的第一个可执行语句。具体语法格式如下。
super([参数列表]);
如果父类的构造方法中包括参数,则参数列表为必选项,用于指定父类构造方法的入口参数。
例如,在实例 2 中的 Animal 类中添加一个默认的构造方法和一个带参数构造方法。
public Animal() {
}
public Animal(String skin) {
this.skin = skin;
}
这时,如果想在子类 Bird 中使用父类带参数的构造方法,则需要在 Bird 中的构造方法中通过以下代码实现:
public Bird() {
super("羽毛");
}
2) 访问被隐藏的成员变量和成员方法
如果想在子类中操作父类中被隐藏的成员变量和成员方法,也可以使用 super 关键字。
语法格式如下:
super.成员变量
super.成员方法([参数列表])
在实例 2 中,如果想在子类 Bird 中改变父类 Animal 的成员变量 skin 的值,可以使用如下代码:
super.skin = “羽毛”;
如果想在子类 Bird 中调用父类 Animal 中的 move() 方法,可以使用如下代码:
super.move();
Java在子类中调用父类构造方法
子类不能继承父类的构造方法。子类在创建新对象时,依次向上寻找其基类,直到找到最初的基类,然后开始执行最初基类的构造方法,再依次向下执行派生类的构造方法,直至执行完最终的扩充类的构造方法为止。
如果子类中没有显式地调用父类的构造方法,那么将自动调用父类中不带参数的构造方法,编译器不再自动生成默认构造方法。如果不在子类构造方法中调用父类带参构造方法,则编译器会因为找不到无参构造方法而报错。
为了解决以上错误,可以在子类显示地调用父类中定义的构造方法,也可以在父类中显示定义无参构造方法。
下面通过一个实例分析怎样在子类中调用父类构造方法。在程序中声明父类 Employee 和子类 CommonEmployee。子类继承父类的非私有属性和方法,但父子类计算各自的工资的方法不同,如父类对象直接获取工资,而子类在底薪的基础上增加奖金数为工资总额。通过子类构造方法中 super 调用类初始化父类的对象,并调用继承父类的方法 toString() 输出员工的基本信息。
class Employee { // 定义父类:雇员类
private String employeeName; // 姓名
private double employeeSalary; // 工资总额
static double mini_salary = 600; // 员工的最低工资
public Employee(String name) { // 有参构造方法
employeeName = name;
System.out.println("父类构造方法的调用。");
}
public double getEmployeeSalary() { // 获取雇员工资
return employeeSalary;
}
public void setEmployeeSalary(double salary) { // 计算员工的薪水
employeeSalary = salary + mini_salary;
}
public String toString() { // 输出员工的基本信息
return ("姓名:" + employeeName + ":工资:" + employeeSalary);
}
}
class CommonEmployee extends Employee { // 定义子类:一般员工类
private double bonus; // 奖金,新的数据成员
public CommonEmployee(String name, double bonus) {
super(name); // 通过 super() 的调用,给父类的数据成员赋初值
this.bonus = bonus; // this 指当前对象
System.out.println("子类构造方法的调用。");
}
public void setBonus(double newBonus) { // 新增的方法,设置一般员工的薪水
bonus = newBonus;
}
// 来自父类的继承,但在子类中重新覆盖父类方法,用于修改一般员工的薪水
public double getEmployeeSalary() {
return bonus + mini_salary;
}
public String toString() {
String s;
s = super.toString(); // 调用父类的同名方法 toString()
// 调用自身对象的方法 getEmployeeSalary(),覆盖父类同名的该方法
return (s + getEmployeeSalary() + "");
}
}
public class TestConstructor { // 主控程序
public static void main(String args[]) {
Employee employee = new Employee("李平"); // 创建员工的一个对象
employee.setEmployeeSalary(1200);
// 输出员工的基本信息
System.out.println("员工的基本信息为:" + employee.toString() + employee.getEmployeeSalary());
// 创建子类一般员工的一个对象
CommonEmployee commonEmployee = new CommonEmployee("李晓云", 500);
// 输出子类一般员工的基本信息
System.out.println("员工的基本信息为:" + commonEmployee.toString());
}
}
程序的运行结果为:
父类构造方法的调用。
员工的基本信息为:姓名:李平:工资:1800.01800.0
父类构造方法的调用。
子类构造方法的调用。
员工的基本信息为:姓名:李晓云:工资:0.01100.0
程序中,在创建子类 CommonEmployee 对象时,父类的构造方法首先被调用,接下来才是子类构造方法的调用;子类对象创建时,为构建父类对象,就必须使用 super() 将子类的实参传递给父类的构造方法,为父类对象赋初值。
关于子类构造方法的使用总结如下:
构造方法不能继承,它们只属于定义它们的类;
创建一个子类对象时,先顺着继承的层次关系向上回溯到最顶层的类,然后向下依次调用每个类的构造方法,最后才执行子类构造方法。