一句话总结
重载是编译时多态(静态分派,同方法名不同参数,看静态类型)→ 重写是运行时多态(动态分派,子类覆盖父类方法,看实际类型)。重写规则:访问权限不能降低、异常不能扩大、static/final/private 方法不能被重写。JVM 通过 vtable(虚方法表)实现动态分派。
初级理解
重载(Overload)— 编译时多态:同一个类中,方法名相同但参数列表不同(参数个数、类型、顺序不同)。返回值类型可以不同,但不能仅靠返回值区分重载。发生在编译期。
重写(Override)— 运行时多态:子类重新定义父类的方法,方法名、参数列表、返回值类型必须完全相同(JDK 5+ 允许协变返回类型)。发生在运行期。
| 对比维度 | 重载(Overload) | 重写(Override) |
| 发生位置 | 同一个类中 | 父子类之间 |
| 参数列表 | 必须不同 | 必须相同 |
| 返回类型 | 可以不同 | 相同或协变 |
| 访问修饰符 | 可以任意 | 不能比父类更严格 |
| 异常 | 可以任意 | 不能抛出更宽泛的异常 |
| 绑定时机 | 编译期(静态绑定) | 运行期(动态绑定) |
// 重载示例
class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public int add(int a, int b, int c) { return a + b + c; }
}
// 重写示例
class Animal {
public void sound() { System.out.println("Animal sound"); }
}
class Dog extends Animal {
@Override // 注解,编译器检查是否正确重写
public void sound() { System.out.println("Woof!"); }
}
中级深入
重写的规则细节:
1. 访问权限不能降低:父类方法是 public,子类不能改成 protected 或 private。
2. 异常不能扩大:子类抛出的受检异常不能比父类更宽泛(可以是父类异常的子类或不抛出)。
3. static 方法不能被重写:static 方法属于类而非实例,子类的同名 static 方法是"隐藏"而非重写。
4. final 方法不能被重写:final 方法禁止子类修改。
5. private 方法不能被重写:private 方法对子类不可见,子类同名方法是全新的方法。
重载的优先级(方法调用时的选择顺序):
public class OverloadPriority {
public void test(Object o) { System.out.println("Object"); }
public void test(String s) { System.out.println("String"); }
// public void test(Integer i) { System.out.println("Integer"); }
public static void main(String[] args) {
OverloadPriority op = new OverloadPriority();
op.test(null); // 输出 "String"
// 编译器会选择最具体的类型:String 比 Object 更具体
}
}
编译器选择重载方法的优先级:精确匹配 → 自动装箱 → 可变参数 → 父类/接口。如果同时有多个匹配且无法判断哪个更具体,编译器会报"ambiguous"错误。
高级拓展
静态分派与动态分派:
静态分派(Static Dispatch)— 重载:编译器根据静态类型(变量声明的类型)决定调用哪个重载方法。发生在编译期。
动态分派(Dynamic Dispatch)— 重写:JVM 根据实际类型(对象运行时的类型)决定调用哪个重写方法。通过方法表(vtable)实现,发生在运行期。
// 静态分派 vs 动态分派
class Parent {}
class Child extends Parent {}
public class DispatchDemo {
void overload(Parent p) { System.out.println("Parent"); }
void overload(Child c) { System.out.println("Child"); }
public static void main(String[] args) {
DispatchDemo demo = new DispatchDemo();
Parent obj = new Child(); // 静态类型 Parent,实际类型 Child
demo.overload(obj); // 输出 "Parent" — 静态分派,看静态类型
// 如果 overload(Child) 不存在,会输出 "Parent"
}
}
方法表(vtable)机制:每个类在方法区中有一张虚方法表,记录了该类所有虚方法(可被重写的方法)的实际入口地址。子类重写父类方法时,会覆盖方法表中对应的条目。JVM 通过 invokevirtual 指令查表实现动态分派。
桥接方法(Bridge Method):当子类重写父类方法时使用了协变返回类型,编译器会自动生成一个桥接方法来保持字节码兼容性。
// Java 源码
class Parent {
public Parent getInstance() { return this; }
}
class Child extends Parent {
@Override
public Child getInstance() { return this; } // 协变返回类型
}
// 编译后,Child 类实际有两个方法:
// 1. public Child getInstance() — 我们写的
// 2. public Parent getInstance() — 编译器生成的桥接方法,内部调用方法1
面试加分项:能说出"重载是静态分派、重写是动态分派",并解释方法表的实现原理,说明你对 JVM 底层有深入理解。
实战场景
场景:重载的自动装箱陷阱
// 陷阱:自动装箱导致调用非预期的重载方法
public class OverloadTrap {
public void remove(int index) {
System.out.println("按索引删除: " + index);
}
public void remove(Integer obj) {
System.out.println("按对象删除: " + obj);
}
public static void main(String[] args) {
OverloadTrap t = new OverloadTrap();
t.remove(1); // "按索引删除: 1"
t.remove(Integer.valueOf(1)); // "按对象删除: 1"
// ArrayList 就有这个坑:remove(int) 按索引,remove(Object) 按元素
}
}
场景:@Override 注解的价值
// 反例:以为在重写,实际是重载
class Parent {
public void doSomething(String name) { }
}
class Child extends Parent {
// 拼写错误或参数不同 → 变成重载而非重写!
public void doSomethng(String name) { } // 编译通过但不重写
}
// 正例:加 @Override,编译器帮你检查
class Child extends Parent {
@Override
public void doSomethng(String name) { } // 编译错误!方法名拼错
}
面试模拟
Q:重载和重写的核心区别是什么?
A:1) 重载在同一个类中,重写在父子类之间;2) 重载看参数列表不同,重写看方法签名相同;3) 重载是编译期静态分派,重写是运行期动态分派;4) 重载与返回值/异常无关,重写有严格限制(权限不降低、异常不扩大)。
Q:什么是桥接方法(Bridge Method)?
A:当子类重写父类方法使用协变返回类型时,编译器自动生成一个桥接方法。例如父类返回 Parent,子类返回 Child,编译器生成一个返回 Parent 的桥接方法,内部调用子类的 Child 版本。这是为了保持字节码层面的方法签名兼容性。