重载与重写有什么区别

候选人小王在字节跳动 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)"。隐藏是静态分派,重写是动态分派,完全不同。

三、核心对比 🔴

维度重载(Overload)重写(Override)
发生位置同一个类父子类之间
方法名相同相同
参数列表必须不同必须相同
返回类型无要求子类可协变
分派时机编译期(静态分派)运行期(动态分派)
JVM 机制根据静态类型选方法根据 vtable 动态查找
多态关系不属于多态实现运行时多态

四、连环追问实战

面试官:"重载和重写都能改变方法行为吗?"

正确回答:"都能改变行为,但方式不同。重载通过参数区分行为,是编译期决定的;重写通过子类替换实现,是运行期决定的。"

面试官:"重载影响多态吗?"

正确回答:"不影响。多态的核心是运行时动态分派,是重写实现的。重载是编译期静态分派,和多态无关。"

面试官:"下面这段代码输出什么?"

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 的声明类型是 BA 中有 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。因为 StringObject 的子类,编译器选择"最具体"的重载版本。

如果两个重载都是同级关系(互不继承),传 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

这是面向对象中最经典的构造顺序陷阱,生产中真实发生过,排查非常困难。