重载与重写有什么区别
候选人小王在字节跳动 1-3 面试,被问到这道"送分题",结果翻车翻得很彻底。
面试官问:"重载和重写有什么区别?"
小王答:"重载是方法名相同参数不同,重写是子类覆盖父类方法。"
面试官点点头,追问:"重载是编译期确定还是运行期确定?"
小王愣了一秒:"运行期?"
面试官又问:"那如果参数类型是父类,实际传入的是子类对象,调用哪个重载版本?"
小王彻底卡住了。
【面试官心理】
重载/重写是基础题,但我追问"编译期还是运行期",是在测试候选人对 Java 类型系统的理解深度。能说出"静态分派"和"动态分派"的,才是真正理解过 JVM 的候选人。
一、重载(Overload)🔴
1.1 本质:静态分派
重载发生在同一个类中,方法名相同,但参数列表不同(类型、个数、顺序任意一个不同都可以)。
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
}
关键:重载在编译期就确定调用哪个方法——这叫静态分派(Static Dispatch)。
1.2 重载的陷阱:类型晋升
void print(int i) { System.out.println("int: " + i); }
void print(long l) { System.out.println("long: " + l); }
void print(double d){ System.out.println("double: " + d); }
byte b = 1;
print(b); // 输出什么?
答案:int: 1。
原因:byte 没有完全匹配的重载,编译器按类型晋升顺序寻找最接近的:byte → short → int → long → float → double → char。
⚠️
类型自动晋升是重载最容易踩坑的地方。print(byte) 会自动匹配到 print(int),而不是 print(long)。
1.3 重载的陷阱:父子类参数
class Animal {}
class Dog extends Animal {}
void process(Animal a) { System.out.println("Animal"); }
void process(Dog d) { System.out.println("Dog"); }
Animal a = new Dog(); // 编译期类型是 Animal
process(a); // 输出什么?
答案:Animal。
原因:重载是静态分派,编译器只看声明类型(静态类型),a 的声明类型是 Animal,所以调用 process(Animal)。运行时实际类型是 Dog,但重载不管这个。
这是重载和重写最本质的区别所在。
【面试官心理】
这道题能直接区分背书和真懂。背书的人只知道"参数列表不同",真懂的人知道"静态分派只看声明类型"。后者面试官会追加分。
二、重写(Override)🔴
2.1 本质:动态分派
重写发生在父子类之间,子类覆盖父类的方法,要求方法签名完全相同。
class Animal {
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("Woof!"); }
}
Animal a = new Dog();
a.speak(); // 输出 "Woof!",而非 "..."
关键:重写在运行期才确定调用哪个方法——这叫动态分派(Dynamic Dispatch),底层依赖虚方法表(vtable)。
2.2 重写的规则(面试必考)
class Parent {
protected Object getValue() throws Exception { return null; }
}
class Child extends Parent {
@Override
public String getValue() throws IOException { // ✅ 合法
return "child";
}
// String 是 Object 的子类(协变返回)
// public 比 protected 更宽松
// IOException 是 Exception 的子类
}
2.3 @Override 注解的价值
class Animal {
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
// 没有 @Override,拼错方法名,编译通过,但没有真正重写!
void speek() { System.out.println("Woof!"); }
}
加上 @Override,编译器会检查是否真的发生了重写,拼错名字会直接报编译错误。
💡
始终加 @Override,这是工程最佳实践。能主动说出这一点的候选人,面试官会认为有代码质量意识。
2.4 不能被重写的方法
// 以下情况无法重写:
// 1. private 方法(子类看不到)
// 2. static 方法(类方法,不走 vtable)
// 3. final 方法(明确禁止重写)
// 4. 构造器(不是普通方法)
class Parent {
private void secretMethod() {} // private,子类隐藏
static void staticMethod() {} // 静态,子类只是隐藏(不是重写)
final void finalMethod() {} // final,不能重写
}
⚠️
static 方法在子类中只是"隐藏(hiding)",不是"重写(overriding)"。隐藏是静态分派,重写是动态分派,完全不同。
三、核心对比 🔴
四、连环追问实战
面试官:"重载和重写都能改变方法行为吗?"
正确回答:"都能改变行为,但方式不同。重载通过参数区分行为,是编译期决定的;重写通过子类替换实现,是运行期决定的。"
面试官:"重载影响多态吗?"
正确回答:"不影响。多态的核心是运行时动态分派,是重写实现的。重载是编译期静态分派,和多态无关。"
面试官:"下面这段代码输出什么?"
class A {
void show(A a) { System.out.println("A.show(A)"); }
void show(B b) { System.out.println("A.show(B)"); }
}
class B extends A {
@Override
void show(A a) { System.out.println("B.show(A)"); }
}
A a = new B();
B b = new B();
a.show(a); // ?
a.show(b); // ?
答案:
a.show(a):a 的运行时类型是 B,调用 B.show(A),输出 B.show(A)(动态分派)
a.show(b):b 的声明类型是 B,A 中有 show(B),且 B 没有重写它,调用 A.show(B),输出 A.show(B)
【面试官心理】
这道混合题是 P6 的必考题。能答对的候选人,基本上对静态分派和动态分派有清晰的区分。答错的候选人,通常会把重载和重写混在一起,按直觉乱猜。
五、生产避坑
5.1 重载陷阱:null 参数
void process(String s) { System.out.println("String"); }
void process(Object o) { System.out.println("Object"); }
process(null); // 输出什么?
答案:String。因为 String 是 Object 的子类,编译器选择"最具体"的重载版本。
如果两个重载都是同级关系(互不继承),传 null 会导致编译错误(ambiguous method call)。
⚠️
方法重载时要谨慎 null 传参,可能导致意外的重载选择。这在生产中出现过 bug,调用了错误的重载版本导致数据处理异常。
5.2 重写陷阱:父类构造器调用虚方法
class Parent {
Parent() {
init(); // 调用了可被重写的方法
}
void init() { System.out.println("Parent.init"); }
}
class Child extends Parent {
private String name = "child";
@Override
void init() {
// name 此时还是 null(字段初始化在父类构造器之后)
System.out.println(name.length()); // NullPointerException!
}
}
new Child(); // 必然 NPE
这是面向对象中最经典的构造顺序陷阱,生产中真实发生过,排查非常困难。