第 09 课:继承、super、方法重写、多态与抽象类

1 阅读14分钟

第 09 课:继承、super、方法重写、多态与抽象类

本课目标

学完后,你应该能够:

  1. 使用 extends 建立合理的父子类关系。
  2. 理解子类继承了什么、不能直接访问什么、构造方法为何不继承。
  3. 使用 super(...) 初始化父类部分,使用 super.method() 调用父类实现。
  4. 使用 @Override 正确重写实例方法。
  5. 区分方法重写与方法重载。
  6. 理解向上转型、编译时类型、运行时类型和动态绑定。
  7. 使用父类数组和父类参数统一处理不同子类。
  8. 使用 instanceof 安全访问子类特有行为。
  9. 定义抽象类与抽象方法。
  10. 判断应该使用继承还是组合。

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();
}

它同时完成:

  1. 判断实际对象是不是 Developer
  2. 创建已经转换好的 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;圆面积是 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,子类重写不能改为 protectedprivate

通过父类引用直接调用子类特有方法

编译器只看父类声明的成员。需要时先用 instanceof,但应优先设计成父类公共行为。

未检查就强制向下转型

实际对象类型不匹配会抛出 ClassCastException

抽象类直接 new

抽象类不完整,不能直接实例化,只能创建具体子类。

把继承当成所有复用的方案

先检查是否存在真正 is-a 关系;has-a 通常使用组合。

20. 检查题与答案

问题

  1. extends 表达什么关系?
  2. Java 类能否同时直接继承两个父类?
  3. 父类的 private 字段是否存在于子类对象中?子类能否直接访问?
  4. 构造方法会被子类继承吗?
  5. super(...) 必须位于什么位置?
  6. @Override 有什么价值?
  7. 重载和重写对参数列表的要求分别是什么?
  8. 抽象类能否有构造方法和普通方法?
  9. 抽象类能否直接 new
  10. Employee e = new Developer(...) 的编译时类型和运行时类型分别是什么?
  11. e.work() 执行父类还是子类版本?
  12. 为什么 e.writeCode() 不能直接编译?
  13. 父类数组为什么能保存不同子类对象?
  14. 未检查就把 Tester 转为 Developer 会发生什么?
  15. instanceof Developer developer 做了哪两件事?
  16. Car extends Engine 为什么通常不合理?
  17. 父类方法调用抽象方法时,运行哪个实现?
  18. 子类重写 public 方法能否改成 private

答案

  1. 子类是父类的一种,即 is-a 关系。
  2. 不能,Java 类只支持单继承;接口可以多实现,下一课学习。
  3. 存在,但子类代码不能直接访问,应通过父类提供的方法使用。
  4. 不会,但创建子类时父类构造仍会先执行。
  5. 子类构造方法的第一条语句。
  6. 让编译器检查该方法是否真的符合重写规则,避免拼写或参数错误。
  7. 重载要求参数列表不同;重写要求方法名和参数列表相同。
  8. 可以。抽象类可以同时拥有字段、构造方法、普通方法和抽象方法。
  9. 不能,只能创建具体子类。
  10. 编译时类型是 Employee,运行时类型是 Developer
  11. 执行 Developer 重写的版本,因为实例方法动态绑定。
  12. 编译时类型 Employee 没有声明 writeCode()
  13. 每个子类对象都是一种父类对象,可以安全向上转型。
  14. 运行时抛出 ClassCastException
  15. 判断实际类型,并创建已转换为 Developer 的局部变量。
  16. 发动机不是汽车的一种,而是汽车拥有的部件,应使用组合。
  17. 根据实际对象类型执行对应子类实现。
  18. 不能,重写方法不能缩小可见性。

21. 本课完成标准

  • 能建立合理的父类与两个子类。
  • 能解释父类构造与子类构造的执行顺序。
  • 能正确使用 @Overridesuper(...)
  • 能区分编译时类型与运行时类型。
  • 能解释“编译看左边、运行看右边”的适用范围。
  • 能使用父类数组和父类参数完成多态处理。
  • 能安全使用 instanceof,并知道不应滥用向下转型。
  • 能定义抽象类和抽象方法。
  • 能根据 is-a 与 has-a 选择继承或组合。

下一课学习接口:interfaceimplements、多实现、默认方法、接口多态,以及用支付策略案例理解“依赖抽象”。