方法重写(Override)
本教程共 100 篇 · 第 40 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
40. 方法重写(Override)
本节目标:搞懂子类怎么改写父类的方法,掌握重写的硬性规则,并且能一眼分清「重写」和「重载」。
子类不满意父类的实现怎么办
继承让子类白捡父类的方法,但捡来的东西不一定合用。
Person 有个 run() 方法,打印「人在跑」。到了 Student 这里,我们想让它打印「学生在跑」。总不能新起个名字叫 runAsStudent() 吧?
Java 的做法是重写(Override):在子类里写一个和父类签名完全相同的方法,把父类的实现盖掉。
class Person {
public void run() {
System.out.println("Person.run");
}
}
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run"); // 盖掉父类的实现
}
}
```bash
调用 `new Student().run()`,输出的是 `Student.run`。父类那份实现被彻底替换了。
> [!NOTE]
> 「重写」和「覆写」「覆盖」说的是同一件事,英文都是 Override。本教程统一用「重写」。
## 什么算「签名相同」
方法签名 = 方法名 + 参数列表(参数类型和顺序)。注意,**返回值不算签名的一部分**。
```java
class Person {
public void run() {}
}
class Student extends Person {
public void run(String s) {} // 参数不同 → 不是重写,是重载
// public int run() {} // 只有返回值不同 → 编译错误!
}
第二行为什么直接报错?因为 Java 不允许在同一个类型体系里出现「名字和参数都一样、只有返回值不同」的两个方法——调用时无法区分。
重写的五条规则
规则不多,但每条都会在编译期咬人。
1. 方法名和参数列表必须完全一致。 差一个参数就变成重载。
2. 返回值类型必须相同,或者是父类返回值的子类。
后半句叫「协变返回类型」(Java 5 引入):
class Person {
public Person copy() { return new Person(); }
}
class Student extends Person {
@Override
public Student copy() { return new Student(); } // 返回 Student,是 Person 的子类,合法
}
```java
**3. 访问权限不能比父类更严格。**
```java
class Person {
public void run() {}
}
class Student extends Person {
// @Override
// protected void run() {} // 编译错误:不能把 public 缩小为 protected
}
放开注释再编译,报错原文是这样:
error: run() in Student cannot override run() in Person
protected void run() {}
^
attempting to assign weaker access privileges; was public
1 error
```java
`attempting to assign weaker access privileges` 这句话就是「你想把权限改小」,见到它去检查修饰符即可。
道理很直白:外界拿着 `Person` 类型的变量在调 `run()`,如果实际对象是 `Student` 而 `run()` 变成了 `protected`,调用就崩了。所以只能放宽,不能收紧。放宽的方向是 `private → 默认 → protected → public`。
| 父类方法权限 | 子类重写可用的权限 |
|-------------|------------------|
| `public` | 只能 `public` |
| `protected` | `protected`、`public` |
| 默认(包私有) | 默认、`protected`、`public` |
| `private` | 子类看不见,谈不上重写 |
**4. 抛出的受检异常不能比父类更宽。**
```java
class Person {
public void run() throws java.io.IOException {}
}
class Student extends Person {
@Override
public void run() throws java.io.FileNotFoundException {} // 更具体的子类异常,合法
}
不能改成 throws Exception,那是扩大了调用方需要处理的范围。也可以完全不抛,那是收窄,永远合法。
5. static、final、private 方法不能被重写。
final方法:父类明确禁止重写(第 42 章细讲)。private方法:子类根本看不见它,在子类写同名方法只是「新方法」,不是重写。static方法:属于类不属于对象,子类写同名静态方法叫「隐藏」(hiding),行为和重写完全不同——它按变量的声明类型走,不看实际对象。
@Override 注解值得每次都写
@Override 不是必须的,去掉照样能跑。但它能救命。
假设你想重写 run(),手抖写成了 runn():
class Student extends Person {
public void runn() { // 拼错了,编译器不吭声
System.out.println("Student.run");
}
}
```java
这段代码编译通过、运行正常,只是你的重写压根没生效。调 `run()` 时执行的还是父类版本,排查半天找不到原因。
加上 `@Override` 就不一样了:
```java
class Student extends Person {
@Override // 编译错误:父类没有 runn() 方法
public void runn() { }
}
编译器会检查「你标注要重写的方法,父类真的有吗」。有错立刻报出来。
报错原文:
error: method does not override or implement a method from a supertype
@Override
^
1 error
```bash
这句话直译就是「这个方法既没重写父类的,也没实现接口的」。九成情况是方法名拼错,或者参数类型对不上(比如父类是 `List` 你写成了 `ArrayList`)。
`@Override` 的第二个价值在改动时。哪天父类把 `run()` 改名成 `move()`,所有标了 `@Override` 的子类会立刻编译报错,提醒你一起改;没标注的则会悄悄退化成一个没人调用的新方法。
> [!TIP]
> 养成习惯:只要是重写,就加 `@Override`。这是零成本的保险。`@Override` 从 Java 5 引入,Java 6 起还支持标注对接口方法的实现。
## 调用父类的原实现:super
重写不一定要完全推翻父类。经常是「在父类基础上加点东西」。
用 `super.方法名()` 可以调到父类被盖掉的那份实现:
```java
class Person {
protected String name;
public Person(String name) { this.name = name; }
public String hello() {
return "Hello, " + name;
}
}
class Student extends Person {
public Student(String name) { super(name); }
@Override
public String hello() {
return super.hello() + "! 我是学生"; // 复用父类逻辑,再追加
}
}
new Student("小明").hello() 输出 Hello, 小明! 我是学生。
关于 super 的完整用法,下一章会专门讲。
重写 vs 重载:一张表说清
这是面试高频题,也是初学者最容易混的两个词。
| 对比项 | 重写 Override | 重载 Overload |
|---|---|---|
| 发生位置 | 子类与父类之间 | 同一个类内部(也可跨父子类) |
| 方法名 | 必须相同 | 必须相同 |
| 参数列表 | 必须相同 | 必须不同 |
| 返回值 | 相同或是子类型 | 无限制 |
| 访问权限 | 不能缩小 | 无限制 |
| 绑定时机 | 运行期(动态) | 编译期(静态) |
一句话记忆:重载是「同名不同参」,重写是「同名同参换实现」。
看个混合示例:
class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; } // 重载
}
class SmartCalculator extends Calculator {
@Override
public int add(int a, int b) { // 重写
System.out.println("智能计算中");
return a + b;
}
}
```bash
`add(int, int)` 和 `add(double, double)` 是重载关系。`SmartCalculator.add(int, int)` 和 `Calculator.add(int, int)` 是重写关系。
## 完整可运行示例
```java
public class Main {
public static void main(String[] args) {
Person p = new Person("小红");
Student s = new Student("小明");
System.out.println(p.hello()); // Hello, 小红
System.out.println(s.hello()); // Hello, 小明! 我是学生
p.run(); // Person.run
s.run(); // Student.run
// 关键:变量声明为父类,实际对象是子类
Person ps = new Student("小刚");
ps.run(); // Student.run —— 执行的是子类版本
}
}
class Person {
protected String name;
public Person(String name) { this.name = name; }
public void run() {
System.out.println("Person.run");
}
public String hello() {
return "Hello, " + name;
}
}
class Student extends Person {
public Student(String name) { super(name); }
@Override
public void run() {
System.out.println("Student.run");
}
@Override
public String hello() {
return super.hello() + "! 我是学生";
}
}
javac Main.java
java Main
```bash
最后三行是重点:`ps` 声明成 `Person`,但实际对象是 `Student`,调用 `run()` 执行的是 `Student` 那份。这个「看实际对象、不看声明类型」的特性叫**多态**,第 43 章专讲。
## 小结
- 重写 = 子类写一个签名和父类完全相同的方法,把实现换掉。
- 五条规则:签名一致、返回值相同或协变、权限不缩小、受检异常不扩大、`static`/`final`/`private` 不可重写。
- `@Override` 让编译器帮你检查重写是否写对,建议每次都加。
- `super.方法()` 可以调用父类被盖掉的实现,用于「在父类基础上扩展」。
- 重载看参数,重写看实现;重载编译期定,重写运行期定。
## 常见疑问
**Q:协变返回类型有什么实际用处?**
链式调用时特别舒服。父类 `copy()` 返回 `Person`,子类重写后返回 `Student`,那么 `new Student("小明").copy().study()` 能直接写;如果返回值卡死成 `Person`,就得先强制转换成 `Student` 才能调子类特有的方法。少一次转换,少一处 `ClassCastException` 的风险。
**Q:子类能不能重写父类的构造方法?**
不能。构造方法名必须和类名一致,子类和父类类名不同,天然没法「签名相同」。子类只能用 `super(...)` 调父类构造,这是下一章的内容。
**Q:`static` 方法写同名的,到底算什么?**
算「隐藏」,不算重写。区别在调用规则:重写看**实际对象**,隐藏看**声明类型**。
```java
Person p = new Student("小明");
p.run(); // 重写 → 走 Student.run(看实际对象)
Person.staticM(); // 隐藏 → 走 Person.staticM(看声明的类)
所以静态方法建议一律用 类名.方法名() 调用,别用对象引用去调,免得被这条规则绕晕。
下一章把 super 的三种用法一次讲透。