×

上一篇文章我们深入了 super 关键字和构造器链,在最后的实战案例中,你可能已经看到了 @Override 的身影。没错,方法重写(Method Override)是继承中最核心的机制之一,也是面向对象第三大特征——多态(Polymorphism)的前提。

这一篇我们就来系统地讲清楚什么是方法重写,它和方法重载有什么区别,以及 final 关键字在继承中扮演了什么角色。

一、什么是方法重写(Override)?

方法重写指的是子类重新定义父类中已有的方法。方法名、参数列表、返回值类型必须与父类方法保持一致(或符合协变返回类型的规则),但方法体由子类重新实现。

class Animal {
    public void sound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("汪汪!");
    }
}

class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("喵喵~");
    }
}

这个例子很典型——动物都会发出声音,但不同动物的声音各不相同。子类通过重写 sound() 方法,给出了各自不同的实现。

二、@Override 注解的作用

@Override 是 Java 提供的注解(Annotation),写在子类方法上,告诉编译器:「我这是在重写父类的方法」。来看它的作用:

  • 编译检查:如果你写的方法签名和父类方法不一致(比如参数类型写错了),编译器会报错,避免了「你以为重写了,实际是新方法」的尴尬。
  • 可读性:看到 @Override,读者立刻知道这个方法是在重写父类逻辑。
class Animal {
    public void sound() {}
}

class Dog extends Animal {
    @Override
    public void sound() {} // ✅ 正确重写

    // @Override
    // public void sound(int volume) {} // ❌ 如果去掉注释会报错,因为这不是重写
}

最佳实践:凡是打算重写父类的方法,一定加上@Override。不加也能重写,但加了能让编译器帮你检查签名是否正
确。这习惯能避免很多手误。

三、重写 vs 重载(Override vs Overload)

这是 Java 新手最容易混淆的一对概念。来一张对比表:

对比维度重写(Override)重载(Overload)
发生位置父子类之间同一个类中
方法签名必须相同必须不同(参数不同)
返回值相同或协变返回类型可以不同
访问修饰符不能比父类更严格无限制
抛出异常不能比父类抛出更宽泛的异常无限制
运行时行为动态绑定(运行时决定调用哪个方法)静态绑定(编译时决定)
关键字@Override

一句话区分:重写看「方法签名完全相同」,重载看「同一个名字但参数不同」

四、重写的规则详解

Java 对方法重写有一系列严格约束,踩到任何一个都会导致编译错误。

规则一:方法签名必须完全一致

方法名 + 参数列表(类型、数量、顺序)必须完全相同。返回值类型可以相同,也可以是协变返回类型(covariant return type)。

class Parent {
    public Number getValue() {
        return 42;
    }
}

class Child extends Parent {
    @Override
    public Integer getValue() { // Integer 是 Number 的子类,合法
        return 100;
    }
}

这就是协变返回类型——Java 5 开始支持,重写方法可以返回父类返回类型的子类。

规则二:访问权限不能更严格

可以保持相同或放宽,但不能收紧:

class Parent {
    protected void method() {}
}

class Child extends Parent {
    @Override
    protected void method() {} // ✅ 相同

    @Override
    public void method() {}    // ✅ 放宽(protected → public)

    // @Override
    // private void method() {} // ❌ 更严格(protected → private),编译错误
}

规则三:不能抛出更宽泛的异常

要么不抛,要么抛出相同或更具体的异常:

import java.io.IOException;
import java.sql.SQLException;

class Parent {
    public void doSomething() throws IOException {}
}

class Child extends Parent {
    @Override
    public void doSomething() throws IOException {}      // ✅ 相同

    @Override
    public void doSomething() {}                          // ✅ 不抛

    // @Override
    // public void doSomething() throws Exception {}     // ❌ Exception 比 IOException 更宽
}

规则四:static 方法不能被重写

静态方法属于类,不属于实例。子类可以定义同名的静态方法,但这叫隐藏(hide),不是重写。它在编译时就已经决定了调用哪个版本。

class Parent {
    public static void greet() {
        System.out.println("Parent 打招呼");
    }
}

class Child extends Parent {
    public static void greet() { // 这是隐藏,不是重写
        System.out.println("Child 打招呼");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        p.greet(); // 输出:Parent 打招呼(编译时绑定)
    }
}

规则五:private 方法不能被重写

private 方法是父类私有的,子类不可见,自然无法重写。子类定义同名方法只是一个新方法,不是重写。

规则六:构造器不能被重写

构造器不是普通方法,不能被继承也不能被重写。

五、动态绑定(Dynamic Binding)

重写之所以能实现,底层依赖 Java 的动态绑定机制(也叫后期绑定、运行时多态)。

简单理解:

  • 编译时:编译器看到的是引用变量的声明类型(比如 Animal a = new Dog() 中的 Animal),它只检查声明类型中是否有这个方法。
  • 运行时:JVM 根据对象的实际类型Dog)来决定调用哪个方法。
Animal a = new Dog();
a.sound(); // 编译时看 Animal 有 sound(),实际调用 Dog 的 sound()

这就是向上转型(Upcasting)与多态的经典搭配。父类引用指向子类对象,调用的却是子类重写后的方法。这个特性是 Java 面向对象的灵魂之一。

六、final 关键字与继承

final 在 Python / JavaScript 等语言中没有直接的对应,但在 Java 里是一个重要的修饰符,用来阻止改变。在继承的上下文中,final 有三个作用对象。

1. final 类:禁止继承

一个类用 final 修饰后,不能被任何类继承。Java 标准库中有一些典型的 final 类:StringIntegerMath 等。

public final class String {
    // ...
}

// class MyString extends String {} // ❌ 编译错误,String 是 final 的

为什么 String 是 final 的?因为如果 String 能被继承,你传一个子类 String 对象到各种方法里,它的行为可能被篡改,破坏安全性——这正是 final 类存在的意义:确保类的行为不可篡改

2. final 方法:禁止重写

子类不能重写父类中的 final 方法。这适用于:有一些方法的行为必须固定下来,不允许子类修改。

class Parent {
    public final void templateMethod() {
        step1();
        step2();
    }

    protected void step1() { /* 子类可重写 */ }
    protected void step2() { /* 子类可重写 */ }
}

class Child extends Parent {
    // @Override
    // public void templateMethod() {} // ❌ 编译错误,final 方法不可重写

    @Override
    protected void step1() { /* 但可以重写非 final 部分 */ }
}

这就是模板方法模式(Template Method Pattern)的雏形——父类定义了算法骨架(final),子类只能定制其中的步骤(非 final)。

3. final 变量:常量

final 修饰的变量一旦赋值就不能再改变:

public class Constants {
    public static final double PI = 3.1415926535; // 类常量
    public final int MAX_SIZE = 100;               // 实例常量
}

public class Main {
    public static void main(String[] args) {
        final int local = 42;
        // local = 100; // ❌ 编译错误

        System.out.println(Constants.PI); // 3.1415926535
    }
}

几点注意:

  • final 实例变量必须在声明时构造器中初始化块中赋值,且只能赋值一次。
  • final 引用变量不能指向其他对象,但对象内部的状态可以改变。
  • static final 组合是 Java 中定义常量的标准方式。

七、代码实战:员工薪资体系(续)

延续上一篇的员工管理案例,加入 final 和一些重写规则的运用:

// 抽象的工资计算基类
abstract class BaseSalaryCalculator {
    // 模板方法:最终的计算流程,子类不可重写
    public final double calculate(Employee emp) {
        double base = getBasePay(emp);
        double bonus = getBonus(emp);
        double tax = calculateTax(base + bonus);
        return base + bonus - tax;
    }

    // 以下两个方法由子类定制
    protected abstract double getBasePay(Employee emp);
    protected abstract double getBonus(Employee emp);

    // 税率固定,不可重写
    private double calculateTax(double amount) {
        return amount * 0.1; // 10% 简单税
    }
}

// 普通员工薪资计算
class RegularSalary extends BaseSalaryCalculator {
    @Override
    protected double getBasePay(Employee emp) {
        return emp.getSalary();
    }

    @Override
    protected double getBonus(Employee emp) {
        return 1000; // 固定奖金
    }
}

// 经理薪资计算
class ManagerSalary extends BaseSalaryCalculator {
    @Override
    protected double getBasePay(Employee emp) {
        return emp.getSalary();
    }

    @Override
    protected double getBonus(Employee emp) {
        return 5000; // 经理固定奖金更高
    }
}

// 员工类
class Employee {
    private String name;
    private double salary;
    private BaseSalaryCalculator calculator;

    public static final double MIN_SALARY = 3000;

    public Employee(String name, double salary, BaseSalaryCalculator calculator) {
        this.name = name;
        this.salary = Math.max(salary, MIN_SALARY);
        this.calculator = calculator;
    }

    public final String getName() { // 名字不允许修改
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public double getActualPay() {
        return calculator.calculate(this);
    }
}

public class Main {
    public static void main(String[] args) {
        RegularSalary regularCalc = new RegularSalary();
        ManagerSalary managerCalc = new ManagerSalary();

        Employee emp1 = new Employee("张三", 8000, regularCalc);
        Employee emp2 = new Employee("李四", 15000, managerCalc);

        System.out.println(emp1.getName() + " 实发:" + emp1.getActualPay());
        System.out.println(emp2.getName() + " 实发:" + emp2.getActualPay());
        System.out.println("最低薪资标准:" + Employee.MIN_SALARY);
    }
}

这个实战案例综合展示了:

  • final 方法calculate()getName() 作为模板方法和只读 API,禁止子类篡改
  • final 变量MIN_SALARY 作为全局常量
  • 方法重写:两个子类重写 getBasePay()getBonus() 实现差异化
  • @Override 注解:让编译器帮我们检查方法签名

const vs final(知识拓展)

有些语言(如 C++ / JavaScript)用 const 来表示「不可更改」。Java 选择了 final 这个关键字来做同样的事情。两者在概念上是相似的,但 Java 的 final 还能修饰类和方法,这是 C++ 的 const 做不到的。

结语

这一篇我们讲了方法重写的完整规则、与重载的对比、动态绑定的原理,以及 final 关键字在继承中的三种使用场景。重写是面向对象多态的基石,而 final 是 Java 用来控制「哪里可以变化,哪里必须固定」的关键工具。

下一篇我们将进行继承专题的最后一篇——继承体系设计实战。会用更接近真实项目的案例,聊一个重要的设计原则(里氏替换原则),并讨论什么时候该用继承、什么时候该用组合。敬请期待!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章

20. 接口基础:语法、特性与抽象类对比

抽象类讲完了。你知道父类可以只声明方法签名,...

读出全部

19. 多态的应用:参数多态化、工厂模式与支付系统实战

上一篇我们拆解了多态的底层机制——动态绑定、...

读出全部

18. 多态的本质:动态绑定、向上转型与向下转型

抽象类学完了,你现在应该知道 「父类只声明、...

读出全部