Java 中方法重写与方法重载的区别
Java 中方法重写与方法重载的区别
[!blue]
- 重载(Overload):同一个类中,方法名相同、参数列表不同,编译期确定调用哪个版本(静态绑定)。
- 重写(Override):子类重新实现父类方法,方法签名必须完全一致,运行期根据对象实际类型确定执行版本(动态绑定)。
- 一句话记忆:重载认参数,重写认对象。
Java 面向对象体系里,重载和重写是最基础也最容易被问倒的一对概念——很多人能背出定义,但一到"父类引用调用子类方法会输出什么"这种题就露馅。这篇文章从对比表格切入,逐层拆到 JVM 的虚方法表,把两者的区别和背后的原理讲透。
一、核心区别
| 对比维度 | 重载 Overload | 重写 Override |
|---|---|---|
| 发生范围 | 同一个类内部 | 父子类之间 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须不同(类型/个数/顺序) | 必须完全相同 |
| 返回值类型 | 可任意 | 相同,或父类返回类型的子类(协变返回) |
| 访问修饰符 | 无限制 | 子类不能比父类更严格 |
| 抛出异常 | 无限制 | 不能抛出比父类更宽泛的受检异常 |
| 绑定时机 | 编译时绑定 | 运行时绑定 |
| 多态类型 | 静态多态 | 动态多态 |
| 能否用于 static/private/final 方法 | 可以 | 不可以(这三类方法不能被重写) |
[!blue]
一句话总结:重载靠"参数列表"在编译期分流,重写靠"对象的实际类型"在运行期分流。
二、方法重载(Overload)详解
2.1 定义与示例
重载指的是同一个类中,允许存在多个方法名相同、但参数列表不同的方法:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
// 参数个数不同。
public int add(int a, int b, int c) {
return a + b + c;
}
// 参数类型不同。
public double add(double a, double b) {
return a + b;
}
}
编译器在编译阶段就能根据实参的类型和个数,精确匹配到对应的方法版本——这也是重载被称为"静态多态"的原因,整个决策过程不依赖任何运行时信息。
2.2 三个必须记住的规则
[!yellow]
- 参数列表必须不同(个数、类型、顺序三选一即可,返回值不算数);
- 仅返回值不同不构成重载,会直接编译报错;
- 可变参数(
...)方法在重载匹配中优先级最低,编译器会优先匹配固定参数版本。
:::fold-red 查看反例:仅返回值不同
// 反例:编译报错,因为参数列表完全相同
public int add(int a, int b) {
return a + b;
}
public double add(int a, int b) {
return a + b;
}
:::
2.3 重载匹配优先级(容易被忽略的细节)
当调用 test(1) 且存在以下几个重载版本时,编译器实际上是按固定顺序去尝试匹配的:
// 优先级 1:精确匹配。
void test(int a) {}
// 优先级 2:自动类型提升(int → long)。
void test(long a) {}
// 优先级 3:自动装箱(int → Integer)。
void test(Integer a) {}
// 优先级 4:可变参数。
void test(int... a) {}
这个优先级顺序在日常开发里经常被忽略,但它正是"为什么调用的不是我以为的那个方法"这类诡异 bug 的根源——尤其是同时存在装箱类型和可变参数重载的时候。
三、方法重写(Override)详解
3.1 定义与示例
重写指的是子类对父类中已有方法的重新实现,方法名、参数列表必须与父类完全一致:
class Animal {
public String makeSound() {
return "一些声音";
}
}
class Dog extends Animal {
@Override
public String makeSound() {
return "汪汪汪";
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.makeSound());
}
}
这里 a 的声明类型是 Animal,但实际输出的是子类重写后的内容——JVM 是在程序运行时,根据 a 指向的对象的实际类型去决定调用哪个版本,这正是重写被称为"动态多态"的原因。
3.2 重写必须遵守的五条规则
[!yellow]
- 方法名、参数列表与父类完全一致;
- 返回值类型相同,或是父类返回类型的子类(协变返回类型);
- 访问权限不能缩小(
public ≥ protected ≥ default ≥ private);- 不能抛出比父类更宽泛的受检异常;
static、private、final方法不能被重写。
:::fold-green 查看合法示例:协变返回类型与受检异常
class Parent {
protected Object getData() throws IOException {
return null;
}
}
class Child extends Parent {
@Override
public String getData() throws FileNotFoundException {
return "hello";
}
}
:::
这五条规则里,最容易踩的坑其实不是记混哪条规则,而是签名写错导致重写悄悄退化成重载——方法能编译通过,但父类引用调用时根本不会触发子类逻辑,排查起来非常隐蔽:
:::fold-red 查看反例:签名写错
class Parent {
public void greet(String name) {}
}
class Child extends Parent {
// 方法签名不一致,实际成为重载。
public void greet() {}
// 加上 @Override 会直接编译报错,暴露这个错误
}
:::
[!yellow]
解决办法很简单:给每一个意图重写的方法都加上@Override,让编译器替你检查签名是否真的匹配,这是零成本但收益很高的习惯。
四、底层原理:为什么重写能实现多态
JVM 为每个类维护一张虚方法表(vtable),子类重写父类方法时,会在自己的虚方法表中用新实现覆盖对应的表项;方法调用发生时,JVM 查的是对象实际类型对应的虚方法表,而不是引用变量的声明类型。
Animal 的 vtable: makeSound → Animal.makeSound()
Dog 的 vtable: makeSound → Dog.makeSound() (覆盖了父类表项)
Animal a = new Dog();
a.makeSound();
→ JVM 查的是 a 指向对象(Dog 实例)的 vtable,而不是 Animal 的
→ 所以执行 Dog.makeSound()
这套机制也解释了为什么 static、private、final 方法不能被重写:
| 方法类型 | 原因 |
|---|---|
static |
属于类本身,不属于对象实例,不进入 vtable,调用时按声明类型直接绑定 |
private |
对子类不可见,根本不会被子类"看到"去覆盖 |
final |
语言层面直接禁止修改 vtable 中的这一项 |
换个角度理解:重载是编译器在做"文字游戏",比对的是方法签名;重写是 JVM 在运行时做"查表游戏",靠的是 vtable 动态分派。理解了 vtable,也就理解了 Java 多态的底层实现方式。
五、高频面试题
:::fold-blue Q1:只有返回值类型不同,算重载吗?
不算,编译会直接报错。方法签名只由"方法名 + 参数列表"决定,返回值不参与签名比较。
:::
:::fold-blue Q2:父类引用调用子类独有的重载方法,会怎样?
class Parent {
public void test(int a) {}
}
class Child extends Parent {
// 重写父类方法。
@Override
public void test(int a) {}
// 子类新增的重载。
public void test(String a) {}
}
Parent p = new Child();
// 调用子类重写版本。
p.test(1);
// 编译报错:Parent 中没有 test(String)。
p.test("hi");
关键在于:重载在编译期按引用的声明类型决定候选方法集合;重写在运行期按对象的实际类型决定具体执行哪个版本。这道题本质上就是在考这句话。
:::
:::fold-blue Q3:static 方法可以被"重写"吗?
不可以。子类定义同签名的 static 方法叫方法隐藏(Hiding),不是重写,调用时依然按引用的声明类型静态绑定,不具备多态性。
:::
六、总结
[!green]
- 重载:同类、同名、参数不同、编译期绑定,体现的是接口的灵活性;
- 重写:父子类、同名、参数相同、运行期绑定(基于 vtable),体现的是多态;
- 记忆口诀:"重载认参数签名,重写认对象类型";
- 排查重写相关 bug 的第一步,永远是检查有没有加
@Override。
理解这两个概念的分水岭,本质上是理解 Java 静态绑定与动态绑定的区别——这也是后续学习设计模式(策略模式、模板方法模式)、读懂框架源码(Spring AOP、集合框架)绕不开的基础。