1. 为什么“重写”和“多态”总被放在一起讲
我当年学Java那会儿,最困惑的一件事就是:明明代码里new的是子类,赋给父类引用之后,调用的却是子类的方法,这到底是怎么发生的?后来工作了几年,看过Spring、MyBatis的源码,又面试过不少人,才发现大多数人其实只记住了“重写要加@Override注解”这个皮毛,对背后的机制理解得很浅。
先说一个反直觉的结论:方法重写(Override)本身不是语法糖,它是Java实现运行时多态的地基。 你可以在不写一个接口、不搞任何设计模式的情况下,只用继承加方法重写,就能让一段代码在运行时根据对象的真实类型表现出不同的行为——这就是多态。面试中常说的“封装、继承、多态”三大特性,多态是最终目的,封装和继承都在为它服务。你去看Spring的ApplicationContext、MyBatis的SqlSession,最核心的机制里都有多态的影子里。
这篇文章适合谁?一个是准备Java面试的人(尤其是那些正在背八股文、想真正理解“重写与多态”的),另一个是写了两三年业务代码、想搞明白框架里那些“看似魔法”的机制的人,还有刚学完Java基础、想知道继承到底有什么用的人。我会从最基础的规则讲到JVM层面的分派机制,再讲到实际项目里怎么用多态写出可扩展的代码,最后附上我自己面试别人和整理八股文时碰到的高频变形题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 方法重写的核心规则以及最容易忽略的边界条件
2.1 到底什么才叫“重写”
方法重写的官方定义是:子类中声明一个方法名、参数列表、返回值类型都与父类方法一致的方法,并且该方法不是静态方法、不是final方法。注意,这里说的是“签名一致”,但返回值的要求有特例——协变返回类型(Covariant Return Type),JDK 5之后,子类可以让返回值变成父类返回类型的子类型。比如父类返回Animal,子类重写后可以返回Dog,只要Dog继承自Animal。
这个点很多人第一次听到会觉得奇怪,但在实际代码里非常有用。看个例子:
java复制public class Animal {
public Animal self() {
return this;
}
}
public class Dog extends Animal {
@Override
public Dog self() { // 返回类型从 Animal 变成 Dog,合法
return this;
}
}
如果Java不允许协变返回类型,你子类想返回更具体的类型就必须写一个全新的方法,没法通过@Override注解告诉编译器“我是来重写的”,代码的连贯性和类型信息都差很多。
2.2 访问修饰符的“向下兼容”问题
重写还有一个经常被忽略的规则:子类重写方法的访问权限不能低于父类方法的访问权限。父类是public,子类就不能是protected,更不能是private或包私有。为什么要有这个限制?道理很简单,既然外部代码能通过父类引用animal.self()调用到你的方法,那子类重写后也必须保证这个调用依然成立,否则多态就崩了。
我见过一个真实案例:有人重写equals方法时,因为子类里的代码逻辑不需要对外暴露,就把权限从public改成了protected,结果编译直接报错。这个错不是编译器的刁难,而是语言规范的一部分——你破坏了一个约定:父类能做的,子类必须也能做。
还有个地方经常踩坑:静态方法不参与重写。子类里写一个和父类静态方法签名一样的方法,叫“隐藏(Hide)”,不是重写。调用哪个方法取决于你用的是哪个类型的引用,而不是对象真实类型。比如:
java复制public class Parent {
public static void hello() {
System.out.println("Parent hello");
}
}
public class Child extends Parent {
public static void hello() {
System.out.println("Child hello");
}
}
// 调用
Parent p = new Child();
p.hello(); // 输出 Parent hello
如果你试着在子类的静态方法上加@Override注解,编译器会直接报错。这个特性和下面的“多态查找”机制形成鲜明对比,面试官相当喜欢拿来挖坑。
2.3 private、final方法真的一点机会都没有吗
private方法在子类里写一个同名同参的方法,编译器不会报错,但它就是在子类里新定义了一个方法,跟父类那个方法没有任何关系。final方法在子类里直接无法重写。这两个规则是硬性的,任何情况下都无法突破。
不过在跟进继承相关的设计时,有个实用的替代方案:父类提供非final的“模板方法”,把要定制化的步骤设计成protected或public的方法,留给子类重写。这就是模板方法模式的雏形,后面会详细讲。你只要记住一个判断标准:一个方法能不能被子类重写,就看它是不是final、是不是private、是不是static,三种情况都不行。
2.4 重写的异常声明契约
子类重写方法时,throws声明的异常必须是父类异常或它的子类,而且不能抛父类方法没声明的新检查型异常。举个例子:
java复制public class Parent {
public void process() throws IOException {
}
}
public class Child extends Parent {
@Override
public void process() throws FileNotFoundException { // 合法,FileNotFoundException 是 IOException 的子类
}
}
反过来,如果父类没声明任何检查型异常,子类重写时也不能新增。这个规则的本质是:既然调用方是按照父类的契约来写代码的,比如只catch了IOException,你子类突然抛出一个SQLException,调用方根本没有资源去处理。这跟访问权限的“向下兼容”如出一辙,都是为了保护多态调用的安全性。
3. 从重写延伸到多态:动态绑定到底在做什么
3.1 编译期类型与运行期类型
理解了重写的规则,接下来要理解多态的核心机制:方法调用的目标是在运行期确定的,不是在编译期确定的。一个引用变量的“编译期类型”是它声明的类型,“运行期类型”是它实际指向的对象类型。Java的多态就是根据运行期类型来决定调用哪个重写方法。
比如:
java复制Animal animal = new Dog();
animal.sound(); // 编译期类型是 Animal,但实际执行的是 Dog.sound()
编译时,编译器检查Animal里有没有sound()方法,没有就报错;运行时,JVM根据animal实际引用的对象类型Dog,找到Dog类里重写的sound()去执行。这一整套过程叫动态绑定,也叫后期绑定(Late Binding)。
相反的是静态绑定,发生在编译期。static方法的调用、
