重载与重写:别再傻傻分不清

在写代码的时候,我见过太多同学把重载(Overload)和重写(Override)搞混。明明面试官问的是重写,你答的全是重载的概念;或者被追问"static方法能不能被重写"时,直接愣住说不出话。

这个问题看似基础,但恰恰是区分"背八股"和"真正理解"的分水岭。今天我们就来把这个知识点彻底讲透。

一、先从一道经典面试题说起

记得有一次面试,候选人小张的简历上写着"熟悉面向对象编程"。我翻到他的项目经验,问了一句:

"重载和重写的区别是什么?"

小张回答得很快:"重载是同名不同参,重写是子类覆盖父类方法。"

听起来没什么问题,对吧?但我追问了一句:"那静态方法能被重写吗?"

小张说:"不能。"

我再问:"为什么不能?静态方法有什么特殊的地方?"

他沉默了三秒,然后说:"因为 static 方法是类方法,不属于实例..."

我说:"好,那你再说说,重载是编译时多态还是运行时多态?"

小张开始支支吾吾。

【面试官心理】 我问他这个问题,不是想刁难他。我想知道的是:他有没有理解重载和重写背后的多态机制。知道"同名不同参"只是表层,知道"编译时绑定 vs 运行时绑定"才是真正理解。静态方法不能被重写,是因为它没有多态性;重载发生在编译阶段,重写发生在运行阶段——这两个知识点必须串起来。

二、重载(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;
    }
}

这个 add 方法有三种形式,调用时编译器会根据传入的参数类型和个数来决定调用哪一个。

2.2 ❌ 错误示范

很多同学会犯这几个错误:

错误1:认为重载和返回类型有关

public class Demo {
    // 这不是重载!编译直接报错
    public int method(int x) {
        return x;
    }

    public double method(int x) {  // 编译错误:与上面的方法重复
        return x;
    }
}

重载只和参数列表有关,和返回类型无关。返回值不同但参数相同的两个方法,编译器会认为它们是重复的。

错误2:认为参数名不同就是重载

public class Demo {
    // 这不是重载!编译直接报错
    public void method(int a) {
        System.out.println(a);
    }

    public void method(int b) {  // 编译错误:参数名不参与重载判断
        System.out.println(b);
    }
}

编译器判断重载只看参数类型和个数,不看参数名称。

错误3:混淆重载和多态

很多同学觉得"重载也是多态的一种",这句话其实不够准确。更精确的说法是:重载是编译时多态(静态绑定),重写是运行时多态(动态绑定)

💡

为什么说重载是"静态绑定"?因为在编译阶段,编译器就已经确定了要调用哪个方法。程序员写代码时,调用的是同一个方法名,但编译器根据参数类型"静态地"决定了最终调用的是哪一个具体实现。

2.3 【直观类比】编译时多态 vs 运行时多态

想象你去自助餐厅吃饭:

  • 编译时多态(重载):菜单上写着"牛排套餐"、"鸡排套餐"、"鱼排套餐",你点餐时服务员直接根据你点的菜给你对应的套餐。在点餐之前(编译时),就已经确定了你吃什么。

  • 运行时多态(重写):你去一家创意餐厅,菜单上只写着"今日特供",具体是什么菜要等厨师做完端上来你才知道。直到菜端上来那一刻(运行时),你才知道今天吃的是牛排还是鸡排。

2.4 标准理解

重载的核心要点:

  1. 同一个类中:不是跨类比较
  2. 方法名相同:这是前提
  3. 参数列表不同:个数、类型、顺序至少有一个不同
  4. 返回类型可以不同:不参与重载判断
  5. 编译时确定:编译器静态绑定
// 编译时就能确定调用哪个方法
int result = calculator.add(1, 2);      // 调用 add(int, int)
double d = calculator.add(1.0, 2.0);   // 调用 add(double, double)

【学习小结】 重载的本质是:同一个名字,不同的参数签名,编译器在编译阶段就决定了调用哪个版本。它解决的是"同名方法但参数不同"的场景,让API更加友好。

三、重写(Override):运行时多态

3.1 什么是重写?

重写,是子类重新定义父类中已有的方法。关键点在于:子类的方法签名必须和父类完全相同

public class Animal {
    public void eat() {
        System.out.println("动物在吃东西");
    }
}

public class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗在吃狗粮");
    }
}

这里子类 Dog 重写了父类 Animaleat() 方法。当用父类引用指向子类对象时:

Animal animal = new Dog();
animal.eat();  // 输出:狗在吃狗粮

调用的到底是父类的 eat() 还是子类的 eat()?这是在运行时决定的,不是编译时。这就是运行时多态

3.2 ❌ 错误示范

错误1:忽略了访问修饰符的规则

public class Parent {
    protected void method() {}
}

public class Child extends Parent {
    // 错误!private 比 protected 更严格,不是重写而是重定义
    private void method() {}  // 编译器可能不会报错,但逻辑上是两个方法
}

子类重写方法的访问修饰符不能比父类更严格private < default < protected < public

错误2:忽略了异常限制

public class Parent {
    public void method() throws IOException {}
}

public class Child extends Parent {
    // 错误!不能抛出比父类更宽泛的异常
    public void method() throws Exception {}  // 编译错误
}

子类重写方法不能抛出比父类更宽泛的受检异常

错误3:忘记了 @Override 注解

很多同学觉得 @Override 只是个标记,可有可无。其实这个注解能帮你在编译期就发现问题:

public class Child extends Parent {
    @Override
    public void feth() {  // 编译错误!父类没有这个方法
        // ...
    }
}

如果你写错了方法名(比如打成 feth),编译器会直接报错。但如果没加 @Override,这会被当成一个新的方法而不是重写,可能导致bug很难发现。

3.3 重写的详细规则

重写必须满足以下所有条件:

条件规则说明
方法名必须相同父类和子类方法名一致
参数列表必须相同参数个数、类型、顺序完全一致
返回类型必须兼容可以是父类返回类型的子类(JDK 5+ 支持协变返回类型)
访问修饰符不能更严格子类 >= 父类
异常声明不能更宽泛子类 <= 父类(仅针对受检异常)
权限不能是 private/static/final这些方法不能被重写

3.4 静态方法:隐藏而非重写

这是一个容易踩坑的地方。静态方法不能被重写,只能被隐藏

public class Parent {
    public static void staticMethod() {
        System.out.println("Parent static method");
    }

    public void instanceMethod() {
        System.out.println("Parent instance method");
    }
}

public class Child extends Parent {
    public static void staticMethod() {  // 这不是重写,是隐藏
        System.out.println("Child static method");
    }

    @Override
    public void instanceMethod() {
        System.out.println("Child instance method");
    }
}

测试一下:

Parent p = new Child();
p.staticMethod();  // 输出:Parent static method
p.instanceMethod();  // 输出:Child instance method

为什么静态方法表现不同?因为:

  • 重写:看对象的实际类型(子类型),运行时多态
  • 隐藏:看引用的声明类型(父类型),编译时决定
Parent p = new Child();
// 静态方法:看"Parent",调用 Parent 的 staticMethod
// 实例方法:看"new Child()",调用 Child 的 instanceMethod
⚠️

静态方法没有多态性,子类的"重写"静态方法本质上是方法隐藏。如果你在面试中被问到"static方法能不能被重写",正确的答案是:不能,它只能被隐藏。这是一个高频踩坑点。

【学习小结】 重写的本质是:子类提供了父类方法的另一种实现,通过"动态绑定"在运行时决定调用哪个版本。它是面向对象"多态性"的核心体现。

四、重载 vs 重写:核心对比

维度重载(Overload)重写(Override)
发生位置同一个类父子类之间
方法签名方法名相同,参数不同方法名和参数都相同
返回类型可以不同必须兼容(可协变)
访问修饰符无限制子类不能比父类更严格
多态类型编译时多态(静态绑定)运行时多态(动态绑定)
关键字无特殊关键字@Override
静态方法可以重载不能重写(只能隐藏)
私有方法可以重载不能重写(会直接忽略)
// 重载示例
class MyClass {
    void method(int a) {}           // 方法A
    void method(String a) {}        // 方法B,参数类型不同
    void method(int a, int b) {}    // 方法C,参数个数不同
    void method(int b, String a) {} // 方法D,参数顺序不同
}

// 重写示例
class Parent {
    void doSomething() {}
}

class Child extends Parent {
    @Override
    void doSomething() {}  // 重写父类方法
}

五、生产场景与避坑指南

5.1 构造函数重载:最常见的应用

构造函数重载是最经典的重载应用:

public class User {
    private String name;
    private int age;

    // 无参构造(默认)
    public User() {
        this.name = "匿名";
        this.age = 0;
    }

    // 只传姓名
    public User(String name) {
        this.name = name;
        this.age = 0;
    }

    // 传姓名和年龄
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

这样设计的好处是:调用方可以根据需要选择合适的构造函数,而不需要记一堆不同的方法名。

5.2 使用重写实现策略模式

在实际项目中,我们经常用重写来实现策略模式:

// 定义支付策略接口
public abstract class PaymentStrategy {
    abstract void pay(double amount);
}

// 支付宝支付
public class AlipayStrategy extends PaymentStrategy {
    @Override
    void pay(double amount) {
        System.out.println("支付宝支付:" + amount);
    }
}

// 微信支付
public class WechatPayStrategy extends PaymentStrategy {
    @Override
    void pay(double amount) {
        System.out.println("微信支付:" + amount);
    }
}

调用时:

PaymentStrategy strategy = new AlipayStrategy();
strategy.pay(100.0);  // 输出:支付宝支付:100.0

strategy = new WechatPayStrategy();
strategy.pay(100.0);  // 输出:微信支付:100.0

这就是典型的运行时多态,不同的子类提供不同的实现,但调用方不需要关心具体是哪个子类。

5.3 ❌ 避坑:构造函数不要调用可能被重写的方法

这是一个经典的坑:

public class Parent {
    public Parent() {
        doSomething();  // 危险!可能调用子类的重写版本
    }

    public void doSomething() {
        System.out.println("Parent doing something");
    }
}

public class Child extends Parent {
    private int value = 1;

    @Override
    public void doSomething() {
        // 此时子类的 field 还没初始化!value 是 0 而不是 1
        System.out.println("Child doing something: " + value);
    }
}

new Child();  // 输出:Child doing something: 0

在父类构造函数中调用可能被子类重写的方法,会导致:子类字段还没初始化、调用到的是子类版本(如果被子类重写)、访问到未初始化的字段。

⚠️

黄金法则:在父类构造函数中,永远不要调用可以被重写的方法。这个坑在生产环境中会导致非常难排查的bug,因为构造函数先于实例字段初始化执行,而重写方法会看到未初始化的数据。

六、面试追问链

第一层:怎么用?

面试官问:"重载和重写的区别是什么?"

标准回答:重载是同一个类中方法名相同但参数列表不同,编译时绑定;重写是子类覆盖父类方法,运行时绑定。

第二层:原理层面

面试官追问:"为什么重载是编译时多态,重写是运行时多态?"

这涉及到 Java 的方法分派机制。重载在编译时就能确定调用哪个版本,因为参数类型是编译时信息。重写需要等到运行时根据实际对象类型来确定,因为子类可能有很多个,编译时无法确定。

第三层:边界问题

面试官追问:"static 方法能重写吗?私有方法能重写吗?"

标准回答:static 方法不能被重写,只能被隐藏(因为没有多态性);私有方法也不能被重写(子类看不到父类的 private 方法),所以子类定义同名方法只是新增了一个方法,不是重写。

第四层:实战应用

面试官追问:"你在项目里怎么用过重写?"

可以举策略模式、模板方法模式的例子,说明重写解决了什么问题、带来了什么价值。

【面试官心理】 我追问 static 和 private 方法,实际上是在试探候选人有没有真正理解重写的本质——它依赖的是"运行时多态",而 private 方法对子类不可见、static 方法没有实例绑定,都不支持多态。知道这些边界的候选人,说明他对多态机制理解得比较透彻。

七、工程建议

  1. 优先使用重写实现多态:当我们需要让不同子类提供不同行为时,重写是首选。它让代码更灵活、扩展性更好。

  2. 构造函数只做赋值:不要在构造函数中调用可能被重写的方法,避免引入难以排查的bug。

  3. 使用 @Override 注解:养成习惯,这能帮助编译器提前发现问题。

  4. 避免过度重载:如果一个类中某个方法有太多重载版本,可能会让调用方困惑。可以考虑使用-builder模式或Optional参数来替代。

【学习小结】 重载和重写虽然名字相近,但本质完全不同。重载是"编译时多态",解决的是"同名方法、不同参数"的可用性问题;重写是"运行时多态",解决的是"同一行为、不同实现"的扩展性问题。理解它们的区别,关键在于理解 Java 的静态绑定和动态绑定机制。