方法重载和方法重写有什么区别?
简化版
重载(overload):同一个类里,方法名相同、参数列表不同,编译期就能确定调哪个,是编译期多态。重写(override):子类给出和父类同签名的新实现,调哪个由运行时对象的真实类型决定,是运行期多态。这么记:重载「横向」比参数,重写「纵向」改实现。
详细版
重载只看参数列表——个数、类型、顺序任一不同即可构成重载。返回值不同不算重载,因为 int f() 和 String f() 调用时都写 f(),编译器没法靠返回值区分。
重写要求方法签名(方法名 + 参数列表)和父类完全一致,此外还有几条约束:
- 返回类型可以是父类返回类型的子类(协变返回);
- 访问权限不能更严(父
public,子不能降成protected); - 抛出的受检异常不能更宽;
- 加
@Override让编译器帮你校验,写错签名会直接报错。
class Animal { void speak() { System.out.println("..."); } }
class Dog extends Animal {
@Override void speak() { System.out.println("汪"); }
}
Animal animal = new Dog();
animal.speak(); // 输出「汪」——运行时按真实类型 Dog 调用
static、private、构造器不能被重写:静态方法属于类、私有方法子类看不见、构造器不参与继承。子类里出现同名静态方法叫「隐藏」,不是重写。
完整版教学
一、编译期多态 vs 运行期多态
这是这道题的题眼。重载在编译期就定死了:编译器根据你传入的实参类型,静态地挑出最匹配的那个重载方法,写进字节码。重写在运行期才定:JVM 看对象实际是什么类型,去调它的方法(动态分派)。
Animal a = new Dog();
a.speak(); // 重写:运行期看 a 实际是 Dog → 调 Dog.speak()
变量声明类型是 Animal,但实际对象是 Dog,最终调的是 Dog 的实现——这就是多态的核心,也是「面向接口编程」能成立的底层机制。
二、一个经典陷阱:重载的匹配在编译期,按「声明类型」
void print(Object o) { System.out.println("Object"); }
void print(String s) { System.out.println("String"); }
Object o = "hello";
print(o); // 输出「Object」,不是「String」!
很多人以为会调 print(String),因为运行时 o 里装的是字符串。但重载解析发生在编译期,编译器只看到 o 的声明类型是 Object,于是绑定 print(Object)。这正好对照上一节:重写看运行时实际类型,重载看编译期声明类型。
三、为什么静态方法不能被重写
重写依赖「运行时动态分派」,而 static 方法在编译期就按声明类型绑定了(静态绑定)。所以:
class Parent { static void f() { System.out.println("Parent"); } }
class Child extends Parent { static void f() { System.out.println("Child"); } }
Parent p = new Child();
p.f(); // 输出「Parent」——静态方法看声明类型,这叫「隐藏」不是「重写」
private 同理——子类根本看不到父类私有方法,谈不上覆盖;final 方法则是显式禁止重写。
四、重写还要同步重写 equals/hashCode 这类「配套」
@Override 不只是防拼错。当你重写 toString()、equals()、hashCode() 时,加上它能确保你真的覆盖了 Object 的方法而不是新写了一个签名不符的方法(比如 equals(User) 而非 equals(Object),那只是重载,容器根本不会调用它)。
记忆点:没写
@Override的「重写」,很可能只是个重载。养成给每个重写方法都加@Override的习惯,编译器会替你把关。
五、重载解析的优先级与歧义
编译器挑重载时会寻找“最具体且可适用”的签名,大体优先精确匹配,其次基本类型拓宽,再考虑装箱,最后才是可变参数。比如传入 int 时,f(long) 往往比 f(Integer) 更优;这不是运行时性能选择,而是语言规范规定的编译期解析结果。
void f(long x) { System.out.println("long"); }
void f(Integer x) { System.out.println("Integer"); }
void f(int... x) { System.out.println("varargs"); }
f(1); // long:基本类型拓宽优先于装箱和可变参数
null 会带来另一类歧义:若同时存在 f(String) 和 f(Integer),调用 f(null) 时两个参数类型互不为子类,编译器无法决定更具体者,直接报错。重载过多会让 API 随新增签名改变解析结果,因此公共 API 应避免“靠隐式转换猜意图”。
| 维度 | 重载 | 重写 |
|---|---|---|
| 决策时间 | 编译期 | 运行期动态分派 |
| 选择依据 | 实参的编译期类型与参数列表 | 接收对象的实际类型 |
| 是否需要继承 | 不需要 | 需要继承或接口实现 |
| 返回值能否单独区分 | 不能 | 可使用协变返回类型 |
| static 方法 | 可重载 | 只能隐藏,不能重写 |
六、常见误区与追问
- 误区:只改变返回类型就能形成重载。 调用表达式无法仅靠期望返回值稳定区分,因此编译器拒绝同参数列表。
- 误区:重载会按运行时对象类型选择。 它只看编译期可见的声明类型,运行时类型参与的是重写分派。
- 误区:子类同名 static 方法是重写。 静态调用按类或声明类型绑定,这叫方法隐藏。
- 追问:重写能扩大访问权限吗? 能,例如 protected 提升为 public;但不能收窄,否则会破坏父类型承诺。
- 追问:重写能抛更宽的受检异常吗? 不能,只能相同、更窄或不抛;非受检异常不受这条限制。
- 追问:为什么必须使用
@Override? 它让编译器验证签名,能抓住把equals(Object)误写成equals(MyType)这类隐蔽重载。
记忆钩子:看到“参数不同”先想到编译器选重载,看到“父引用指向子对象”再想到 JVM 选重写。
还有一个设计信号:如果调用者经常需要强制类型转换才能选中某个重载,说明方法名或参数模型可能过度含糊。将不同语义改成不同方法名,或引入参数对象,通常比继续叠加重载更容易维护。
接口默认方法和抽象方法同样可以被实现类重写,判断标准仍是实例方法签名与运行时分派;不要把“重写”只限定为普通父类与子类之间的关系。
七、加强记忆
重载是在同一可见范围内为同名方法提供不同参数入口,编译器依据声明类型和转换优先级提前绑定;重写则由子类型替换继承来的实例方法实现,JVM 根据接收对象实际类型动态分派。static、private 和构造器不参与重写,返回值也不能单独构成重载。用 @Override 固化意图,并控制重载数量,能同时避免签名写错和解析歧义。