Java继承机制详解(图文并茂,附带实例)
在 Java 的面向对象程序设计中,继承是不可或缺的一部分。通过继承可以实现代码的复用,提高程序的可维护性。
在现实世界中的对象存在很多如下图的关系:

图 1 不同车之间的关系
巴士、卡车和出租车都是汽车的一种,分别拥有相似的特性,如对所有的交通工具而言都具备引擎数量、外观的颜色,相似的行为如刹车和加速的功能。但每种不同的交通工具又有自己的特性,例如:
面向对象的程序设计中该怎样描述现实世界中的这种情况呢?这就要用到继承的概念。继承就是从已有的类派生出新的类。新的类能吸收已有类的数据属性和行为,并能扩展新的能力。
已有的类一般称为父类(基类或超类)。由基类产生的新类称为派生类或子类。派生类同样也可以作为基类再派生新的子类,这样就形成了类间的层次结构。修改后的交通工具间的继承关系如下图所示。

图 2 继承关系
汽车被抽象为父类(基类或超类),代表一般化属性。而巴士、卡车和出租车转化为子类,继承父类的一般特性包括父类的数据成员和行为,如外观颜色和刹车等特性,又产生自己独特的属性和行为,如巴士的最大载客数和报站。
继承的方式包括单一继承和多重继承:
两种继承方式如下图所示:

图 3 继承的方式
注意,Java 语言只支持单一继承,不支持多重继承。
例如设计并实现教师类,其中教师分为 Java 教师以及 .NET 教师,各自的要求如下:
在这个例子中,可以将 JavaTeacher 类和 DotNetTeacher 类的共同点抽取出来,形成一个 Teacher 类,代码如下:
Java 中,子类继承父类的语法格式如下:
使用继承实现以上代码【实例 1】:
TestTeacher 的 main() 方法中声明两个子类对象。子类对象分别调用各自的方法进行授课和自我介绍。如语句 javaTeacher.giveLesson(),就调用 JavaTeacher 子类的方法实现授课的处理,该子类的方法来自对父类 Teacher 方法 giveLesson() 的继承,语句 super.giveLesson() 代表对父类同名方法的调用。
在实例 1 中,父类 Teacher 中定义了 giveLesson() 方法,但是两个子类也各自定义了自己的 giveLesson() 方法。
在进行方法覆盖时,特别需要注意,子类在覆盖父类方法时应注意以下几点:
另外,需要注意方法重载与方法覆盖的区别:
总之,子类可以继承父类中所有可被子类访问的成员变量和成员方法,但必须遵循以下原则:
【实例 2】定义一个动物类 Animal,包含两个成员变量 live 和 skin 以及两个成员方法 eat() 和 move();再定义 Animal 的子类 Bird,在该类中隐藏父类的成员变量 skin,覆盖父类的 move() 方法,并定义测试类进行测试。
程序运行结果为:
类的传递性对 Java 语言有重要的意义。如下代码演示了继承的传递性,定义了三个类 Vehicle、Trunk、SmallTruck,其中类 Trunk 继承 Vehicle,类 SmallTruck 继承 Trunk,并测试 SmallTruck 可以继承 Vehicle 的成员。
从本例可以看出,Java 语言的继承关系既解决了代码复用的问题,又表示了一个体系,这是面向对象中继承真正的作用。运行该程序,执行结果为:
例如,在实例 2 中的 Animal 类中添加一个默认的构造方法和一个带参数构造方法。
语法格式如下:
在实例 2 中,如果想在子类 Bird 中改变父类 Animal 的成员变量 skin 的值,可以使用如下代码:
如果子类中没有显式地调用父类的构造方法,那么将自动调用父类中不带参数的构造方法,编译器不再自动生成默认构造方法。如果不在子类构造方法中调用父类带参构造方法,则编译器会因为找不到无参构造方法而报错。
为了解决以上错误,可以在子类显示地调用父类中定义的构造方法,也可以在父类中显示定义无参构造方法。
下面通过一个实例分析怎样在子类中调用父类构造方法。在程序中声明父类 Employee 和子类 CommonEmployee。子类继承父类的非私有属性和方法,但父子类计算各自的工资的方法不同,如父类对象直接获取工资,而子类在底薪的基础上增加奖金数为工资总额。通过子类构造方法中 super 调用类初始化父类的对象,并调用继承父类的方法 toString() 输出员工的基本信息。
关于子类构造方法的使用总结如下:
在现实世界中的对象存在很多如下图的关系:

图 1 不同车之间的关系
巴士、卡车和出租车都是汽车的一种,分别拥有相似的特性,如对所有的交通工具而言都具备引擎数量、外观的颜色,相似的行为如刹车和加速的功能。但每种不同的交通工具又有自己的特性,例如:
- 巴士拥有和其他交通工具不同的特性和行为,即最大载客数量和到指定站点要报站的特点;
- 卡车的主要功能是运送货物,也就是载货和卸货,因此拥有最大载重的特性。
面向对象的程序设计中该怎样描述现实世界中的这种情况呢?这就要用到继承的概念。继承就是从已有的类派生出新的类。新的类能吸收已有类的数据属性和行为,并能扩展新的能力。
已有的类一般称为父类(基类或超类)。由基类产生的新类称为派生类或子类。派生类同样也可以作为基类再派生新的子类,这样就形成了类间的层次结构。修改后的交通工具间的继承关系如下图所示。

图 2 继承关系
汽车被抽象为父类(基类或超类),代表一般化属性。而巴士、卡车和出租车转化为子类,继承父类的一般特性包括父类的数据成员和行为,如外观颜色和刹车等特性,又产生自己独特的属性和行为,如巴士的最大载客数和报站。
继承的方式包括单一继承和多重继承:
- 单一继承是最简单的方式,一个派生类只从一个基类派生;
- 多重继承是一个派生类有两个或多个基类。
两种继承方式如下图所示:

图 3 继承的方式
通过上面介绍可以看出基类与派生类的关系:图中箭头的方向表示继承的方向,由子类指向父类。
- 基类是派生类的抽象(基类抽象了派生类的公共特性);
- 派生类是对基类的扩展;
- 派生类和基类的关系相当于“是一个(is a)”的关系,即派生类是基类的一个对象,而不是“有(has)”的组合关系,即类的对象包含一个或多个其他类的对象作为该类的属性,如汽车类拥有发动机、轮胎,这种关系称为类的组合。
注意,Java 语言只支持单一继承,不支持多重继承。
例如设计并实现教师类,其中教师分为 Java 教师以及 .NET 教师,各自的要求如下:
Java教师:
属性:姓名、所属部门
方法:授课(打开 Eclipse、实施理论课授课)、自我介绍
.NET教师:
属性:姓名、所属部门
方法:授课(打开 Visual studio 2010、实施理论课授课)、自我介绍
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
知识点讲解
总结提问
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
关于子类构造方法的使用总结如下:
- 构造方法不能继承,它们只属于定义它们的类;
- 创建一个子类对象时,先顺着继承的层次关系向上回溯到最顶层的类,然后向下依次调用每个类的构造方法,最后才执行子类构造方法。