← 返回题目列表

方法重载和方法重写有什么区别?

高频 简单 第 2 / 32 题 更新于 2026/07/26
面向对象重载重写

简化版

重载(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 调用

staticprivate、构造器不能被重写:静态方法属于类、私有方法子类看不见、构造器不参与继承。子类里出现同名静态方法叫「隐藏」,不是重写。

完整版教学

一、编译期多态 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 固化意图,并控制重载数量,能同时避免签名写错和解析歧义。