- 0
- 535 words
上一篇文章我们深入了 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 类:String、Integer、Math 等。
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 用来控制「哪里可以变化,哪里必须固定」的关键工具。
下一篇我们将进行继承专题的最后一篇——继承体系设计实战。会用更接近真实项目的案例,聊一个重要的设计原则(里氏替换原则),并讨论什么时候该用继承、什么时候该用组合。敬请期待!