第 09 课:继承、super、方法重写、多态与抽象类
本课目标
学完后,你应该能够:
- 使用
extends建立合理的父子类关系。 - 理解子类继承了什么、不能直接访问什么、构造方法为何不继承。
- 使用
super(...)初始化父类部分,使用super.method()调用父类实现。 - 使用
@Override正确重写实例方法。 - 区分方法重写与方法重载。
- 理解向上转型、编译时类型、运行时类型和动态绑定。
- 使用父类数组和父类参数统一处理不同子类。
- 使用
instanceof安全访问子类特有行为。 - 定义抽象类与抽象方法。
- 判断应该使用继承还是组合。
1. 为什么需要继承
开发工程师与测试工程师都有姓名和基础工资,也都有工作、计算工资和显示信息的行为。如果完全分开编写,会重复大量代码。
继承把真正共有的状态与行为放进父类:
Employee
/ \
Developer Tester
public class Developer extends Employee {
}
extends 表示 Developer 是一种 Employee。这叫 is-a 关系:
- Developer is an Employee:合理;
- Tester is an Employee:合理;
- Engine is a Car:不合理,发动机不是汽车,而是汽车拥有发动机。
继承不是为了少写几行代码就强行使用。只有子类确实能够替代父类、满足 is-a 关系时才适合继承。
2. Java 类的继承规则
- 一个类只能直接继承一个类,Java 类不支持多继承。
- 一个类可以有很多子类。
- 子类还可以继续有子类,形成继承层次。
- 所有类最终都继承自
Object。 - 构造方法不会被继承。
- 父类的
private字段仍存在于子类对象内部,但子类代码不能直接访问。
因此本课父类字段仍保持 private:
private String name;
private double baseSalary;
子类通过 getter 使用:
getName();
getBaseSalary();
不要为了让子类方便就把所有字段改成 public。封装在继承中仍然重要。
3. 子类对象中有什么
Developer developer = new Developer("小明", 12000, 10, 100);
这个对象同时包含:
- 从
Employee获得的姓名与基础工资部分; Developer自己增加的加班小时与加班单价部分;- 父类提供的普通方法;
- 子类新增和重写的方法。
可以把一个子类对象理解为“父类规定的共同部分 + 子类扩展的专有部分”,但它仍是一个完整对象,不是两个对象拼在一起。
4. super(...) 与构造顺序
父类构造方法不会继承,但创建子类对象时必须先初始化父类部分:
public Developer(String name, double baseSalary,
int overtimeHours, double overtimeRate) {
super(name, baseSalary);
this.overtimeHours = overtimeHours;
this.overtimeRate = overtimeRate;
}
super(name, baseSalary) 调用父类对应构造方法,规则如下:
- 必须位于子类构造方法的第一条语句;
- 父类构造先执行,子类构造后执行;
- 如果没有明确写
super(...),编译器尝试自动插入super(); - 如果父类没有无参构造,子类就必须明确调用现有的带参父类构造。
构造顺序:
new Developer(...)
↓
Employee 构造方法
↓
Developer 构造方法其余部分
↓
对象创建完成
同一个构造方法不能同时调用 this(...) 和 super(...),因为二者都要求成为第一条语句。
5. super 的其他用途
super 表示当前对象中的父类部分。
调用父类方法:
@Override
public void work() {
super.work();
System.out.println("开始编写代码");
}
当子类想保留父类行为并添加额外逻辑时使用 super.method()。
如果父类字段是 protected,子类可以通过 super.field 或直接访问。但实际项目中仍常优先使用 private + getter,因为 getter 能保留封装和未来修改空间。
6. 方法重写
父类已有方法,子类提供符合自身特点的新实现,称为重写:
public class Employee {
public void work() {
System.out.println("完成通用工作");
}
}
public class Developer extends Employee {
@Override
public void work() {
System.out.println("编写 Java 代码");
}
}
基本规则:
- 方法名相同;
- 参数列表相同;
- 返回值类型相同或满足协变返回规则;
- 子类访问权限不能比父类更严格;
static方法不参与运行时多态,它属于隐藏,不是实例方法重写;private方法对子类不可见,因此不能被真正重写;final方法不能被重写。
强烈建议写 @Override。它不是给程序运行使用的,而是让编译器帮你发现拼写错误和参数不匹配:
@Override
public void work() {
}
7. 重载与重写的区别
| 对比 | 重载 Overload | 重写 Override |
|---|---|---|
| 出现位置 | 通常同一个类,也可涉及继承 | 父类与子类之间 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须不同 | 必须相同 |
| 作用 | 同一功能接收不同参数 | 子类替换父类行为 |
| 决定时间 | 编译期 | 实例重写版本在运行期动态选择 |
| 典型示例 | add(int,int) 与 add(double,double) | Employee.work() 与 Developer.work() |
8. 抽象类与抽象方法
“员工”只是共同概念,无法给出所有员工统一的工资公式。可以将父类声明为抽象类:
public abstract class Employee {
public abstract double calculateMonthlySalary();
}
抽象方法:
- 使用
abstract; - 只有方法声明,没有
{}方法体; - 以分号结尾;
- 具体子类必须实现,除非子类自己也是抽象类。
抽象类:
- 不能直接
new Employee(); - 可以有字段;
- 可以有构造方法;
- 可以有普通方法;
- 可以有抽象方法;
- 可以声明父类类型的引用。
虽然不能直接创建抽象父类对象,但父类构造方法仍会在创建子类时通过 super(...) 执行。
9. 多态与向上转型
Developer developer = new Developer(...);
Employee employee = developer;
子类对象赋给父类引用称为向上转型,通常自动且安全,因为开发工程师一定也是员工。
还可以一步完成:
Employee employee = new Developer(...);
此时:
- 编译时类型:
Employee,由左侧声明决定; - 运行时类型:
Developer,由右侧实际对象决定。
10. 能调用什么与执行哪个版本
这是多态最重要的规则。
Employee employee = new Developer(...);
能调用什么
编译器根据引用的编译时类型 Employee 判断。因此可以调用 Employee 声明的:
employee.work();
employee.calculateMonthlySalary();
employee.getName();
不能直接调用只在 Developer 中存在的方法:
employee.writeCode(); // 编译错误
实际执行哪个版本
如果方法被子类重写,运行时根据实际对象类型选择:
employee.work();
引用类型虽然是 Employee,实际对象是 Developer,因此执行 Developer.work()。这称为动态绑定或动态分派。
口诀:
编译看左边:决定允许调用哪些成员
运行看右边:决定重写方法执行哪个版本
这个口诀主要针对实例方法。字段和静态方法不具备同样的多态行为。
11. 父类数组与父类参数
Employee[] employees = {
new Developer(...),
new Tester(...)
};
for (Employee employee : employees) {
employee.work();
System.out.println(employee.calculateMonthlySalary());
}
同一段循环无需判断每个对象的具体类型。新增 Designer extends Employee 后,只要实现抽象方法,原循环通常不需要修改。
父类参数也一样:
public static void printPayroll(Employee employee) {
System.out.println(employee.calculateMonthlySalary());
}
它可以接收任意 Employee 子类。后端开发中的 service 接口、支付方式、消息发送器等大量使用这种“依赖抽象”的思想。
12. 向下转型与 instanceof
父类引用不能直接调用子类特有方法。确实需要时,可以在检查后向下转型。
传统写法:
if (employee instanceof Developer) {
Developer developer = (Developer) employee;
developer.writeCode();
}
Java 17 支持模式匹配:
if (employee instanceof Developer developer) {
developer.writeCode();
}
它同时完成:
- 判断实际对象是不是
Developer; - 创建已经转换好的
developer局部变量。
未经检查的错误转换会产生 ClassCastException:
Employee employee = new Tester(...);
Developer developer = (Developer) employee; // 运行时错误
如果代码到处都在使用 instanceof,往往说明多态设计还可以改进。优先把共同操作放进父类抽象方法。
13. 完整源码一:Employee.java
public abstract class Employee {
// 父类保存所有员工共有的状态。
// 字段保持 private,子类通过 getter 使用,继续遵守封装。
private String name;
private double baseSalary;
public Employee(String name, double baseSalary) {
setName(name);
setBaseSalary(baseSalary);
}
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
this.name = "未知员工";
return;
}
this.name = name.strip();
}
public double getBaseSalary() {
return baseSalary;
}
public void setBaseSalary(double baseSalary) {
this.baseSalary = Math.max(baseSalary, 0.0);
}
// 普通实例方法已经有实现,子类直接继承,也可以选择重写。
public void work() {
System.out.println(name + " 正在完成通用工作。");
}
// 抽象方法只有规则,没有方法体;具体子类必须给出实现。
public abstract String getRole();
public abstract double calculateMonthlySalary();
// 父类方法内部调用抽象方法时,也会执行实际子类的实现。
public void printInfo() {
System.out.println("姓名:" + name);
System.out.println("岗位:" + getRole());
System.out.println("月薪:" + calculateMonthlySalary());
}
}
父类同时包含完整行为与抽象规则。printInfo() 虽然写在父类中,但其中调用的 getRole() 和 calculateMonthlySalary() 会动态执行子类版本。
14. 完整源码二:Developer.java
public class Developer extends Employee {
private int overtimeHours;
private double overtimeRate;
public Developer(String name, double baseSalary,
int overtimeHours, double overtimeRate) {
super(name, baseSalary);
setOvertimeHours(overtimeHours);
setOvertimeRate(overtimeRate);
}
public int getOvertimeHours() {
return overtimeHours;
}
public void setOvertimeHours(int overtimeHours) {
this.overtimeHours = Math.max(overtimeHours, 0);
}
public double getOvertimeRate() {
return overtimeRate;
}
public void setOvertimeRate(double overtimeRate) {
this.overtimeRate = Math.max(overtimeRate, 0.0);
}
@Override
public void work() {
System.out.println(getName() + " 正在编写和审查 Java 代码。");
}
@Override
public String getRole() {
return "Java 开发工程师";
}
@Override
public double calculateMonthlySalary() {
return getBaseSalary() + overtimeHours * overtimeRate;
}
public void writeCode() {
System.out.println(getName() + " 提交了一次代码变更。");
}
}
Developer 继承共同状态,增加加班字段,并实现父类抽象方法。writeCode() 是子类特有方法,不属于所有员工。
15. 完整源码三:Tester.java
public class Tester extends Employee {
private int foundBugCount;
private double bonusPerBug;
public Tester(String name, double baseSalary,
int foundBugCount, double bonusPerBug) {
super(name, baseSalary);
setFoundBugCount(foundBugCount);
setBonusPerBug(bonusPerBug);
}
public void setFoundBugCount(int foundBugCount) {
this.foundBugCount = Math.max(foundBugCount, 0);
}
public void setBonusPerBug(double bonusPerBug) {
this.bonusPerBug = Math.max(bonusPerBug, 0.0);
}
@Override
public void work() {
System.out.println(getName() + " 正在设计测试用例并报告缺陷。");
}
@Override
public String getRole() {
return "软件测试工程师";
}
@Override
public double calculateMonthlySalary() {
return getBaseSalary() + foundBugCount * bonusPerBug;
}
public void runRegressionTest() {
System.out.println(getName() + " 完成了回归测试。");
}
}
测试工程师与开发工程师使用相同父类协议,但工资公式和工作行为不同。这正是多态需要解决的问题。
16. 完整源码四:EmployeeDemo.java
public class EmployeeDemo {
public static void main(String[] args) {
Developer developer = new Developer("小明", 12000.0, 10, 100.0);
Tester tester = new Tester("小红", 10000.0, 8, 50.0);
// 向上转型:子类对象可以安全地由父类引用保存。
Employee firstEmployee = developer;
Employee secondEmployee = tester;
// 父类数组可以统一保存不同的子类对象。
Employee[] employees = {firstEmployee, secondEmployee};
for (Employee employee : employees) {
System.out.println("===== 员工信息 =====");
// 动态绑定:运行时根据真实对象类型选择重写方法。
employee.work();
employee.printInfo();
printPayroll(employee);
// Java 17 模式匹配,同时完成类型判断和转换。
if (employee instanceof Developer actualDeveloper) {
actualDeveloper.writeCode();
}
System.out.println();
}
}
// 父类参数可以接收所有 Employee 子类对象。
public static void printPayroll(Employee employee) {
System.out.println(employee.getName()
+ " 的应发工资为:"
+ employee.calculateMonthlySalary());
}
}
运行结果推演
开发工程师工资:
12000 + 10 × 100 = 13000
测试工程师工资:
10000 + 8 × 50 = 10400
循环变量始终声明为 Employee,但第一次实际对象是 Developer,第二次是 Tester。因此相同调用产生不同结果。
17. 独立练习:图形体系
需求:
- 抽象父类
Shape保存颜色; - 所有图形必须计算面积与周长;
Circle使用圆公式;Rectangle使用矩形公式;- 使用
Shape[]统一遍历并累计总面积。
Shape.java 完整答案
public abstract class Shape {
private String color;
public Shape(String color) {
setColor(color);
}
public String getColor() {
return color;
}
public void setColor(String color) {
if (color == null || color.isBlank()) {
this.color = "未指定";
return;
}
this.color = color.strip();
}
public abstract double calculateArea();
public abstract double calculatePerimeter();
public void printSummary() {
System.out.println("图形类型:" + getClass().getSimpleName());
System.out.println("颜色:" + color);
System.out.println("面积:" + calculateArea());
System.out.println("周长:" + calculatePerimeter());
}
}
Shape 不知道具体公式,所以把面积和周长声明为抽象方法。getClass().getSimpleName() 来自 Object,运行时返回实际类的简单名称。
Circle.java 完整答案
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
setRadius(radius);
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = Math.max(radius, 0.0);
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
@Override
public double calculatePerimeter() {
return 2 * Math.PI * radius;
}
}
Rectangle.java 完整答案
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String color, double width, double height) {
super(color);
setWidth(width);
setHeight(height);
}
public void setWidth(double width) {
this.width = Math.max(width, 0.0);
}
public void setHeight(double height) {
this.height = Math.max(height, 0.0);
}
@Override
public double calculateArea() {
return width * height;
}
@Override
public double calculatePerimeter() {
return 2 * (width + height);
}
}
ShapeDemo.java 完整答案
public class ShapeDemo {
public static void main(String[] args) {
// 同一个 Shape 数组保存两种不同的具体图形。
Shape[] shapes = {
new Circle("红色", 3.0),
new Rectangle("蓝色", 4.0, 5.0)
};
double totalArea = 0.0;
for (Shape shape : shapes) {
shape.printSummary();
// 多态调用实际子类的面积公式。
totalArea += shape.calculateArea();
System.out.println();
}
System.out.println("全部图形总面积:" + totalArea);
}
}
矩形面积是 20;圆面积是 9π。ShapeDemo 不需要写 if (shape 是圆),直接调用抽象方法即可,这就是多态带来的统一处理能力。
18. 继承与组合如何选择
继承表示稳定的 is-a 关系,组合表示 has-a 关系。
class Car {
private Engine engine;
}
汽车拥有发动机,所以使用组合,不写 Car extends Engine。
优先考虑组合的原因:
- 继承会让子类与父类实现强耦合;
- 父类变化可能影响所有子类;
- Java 类只能单继承;
- 组合更容易在运行时替换部件。
但当子类真正能替代父类,并且需要多态协议时,继承仍然合适,例如 Circle 是一种 Shape。
19. 常见错误
子类构造没有正确调用父类构造
父类没有无参构造时,子类必须明确 super(...)。
参数不同却以为是重写
public void work(String task)
如果父类是 work(),这不是重写,而是新增重载。@Override 会帮你发现。
降低访问权限
父类方法是 public,子类重写不能改为 protected 或 private。
通过父类引用直接调用子类特有方法
编译器只看父类声明的成员。需要时先用 instanceof,但应优先设计成父类公共行为。
未检查就强制向下转型
实际对象类型不匹配会抛出 ClassCastException。
抽象类直接 new
抽象类不完整,不能直接实例化,只能创建具体子类。
把继承当成所有复用的方案
先检查是否存在真正 is-a 关系;has-a 通常使用组合。
20. 检查题与答案
问题
extends表达什么关系?- Java 类能否同时直接继承两个父类?
- 父类的
private字段是否存在于子类对象中?子类能否直接访问? - 构造方法会被子类继承吗?
super(...)必须位于什么位置?@Override有什么价值?- 重载和重写对参数列表的要求分别是什么?
- 抽象类能否有构造方法和普通方法?
- 抽象类能否直接
new? Employee e = new Developer(...)的编译时类型和运行时类型分别是什么?e.work()执行父类还是子类版本?- 为什么
e.writeCode()不能直接编译? - 父类数组为什么能保存不同子类对象?
- 未检查就把
Tester转为Developer会发生什么? instanceof Developer developer做了哪两件事?Car extends Engine为什么通常不合理?- 父类方法调用抽象方法时,运行哪个实现?
- 子类重写
public方法能否改成private?
答案
- 子类是父类的一种,即 is-a 关系。
- 不能,Java 类只支持单继承;接口可以多实现,下一课学习。
- 存在,但子类代码不能直接访问,应通过父类提供的方法使用。
- 不会,但创建子类时父类构造仍会先执行。
- 子类构造方法的第一条语句。
- 让编译器检查该方法是否真的符合重写规则,避免拼写或参数错误。
- 重载要求参数列表不同;重写要求方法名和参数列表相同。
- 可以。抽象类可以同时拥有字段、构造方法、普通方法和抽象方法。
- 不能,只能创建具体子类。
- 编译时类型是
Employee,运行时类型是Developer。 - 执行
Developer重写的版本,因为实例方法动态绑定。 - 编译时类型
Employee没有声明writeCode()。 - 每个子类对象都是一种父类对象,可以安全向上转型。
- 运行时抛出
ClassCastException。 - 判断实际类型,并创建已转换为
Developer的局部变量。 - 发动机不是汽车的一种,而是汽车拥有的部件,应使用组合。
- 根据实际对象类型执行对应子类实现。
- 不能,重写方法不能缩小可见性。
21. 本课完成标准
- 能建立合理的父类与两个子类。
- 能解释父类构造与子类构造的执行顺序。
- 能正确使用
@Override和super(...)。 - 能区分编译时类型与运行时类型。
- 能解释“编译看左边、运行看右边”的适用范围。
- 能使用父类数组和父类参数完成多态处理。
- 能安全使用
instanceof,并知道不应滥用向下转型。 - 能定义抽象类和抽象方法。
- 能根据 is-a 与 has-a 选择继承或组合。
下一课学习接口:interface、implements、多实现、默认方法、接口多态,以及用支付策略案例理解“依赖抽象”。