重载与重写的深浅,从聊坑开始见分晓。这俩名字像,却被绑定在编译期和运行期两个完全不同的时刻,背资料的人在这里往往张冠李戴。默认行为是什么、边界在哪、出错时是什么表现,把它们讲清楚,这题的分量就出来了。
先把结论放在前面:重载(overload)是同一类里"方法名相同、参数列表不同",由编译器在编译期按签名静态决议;重写(override)是子类提供与父类"同签名(含协变返回)"的方法,由 JVM 在运行期按对象实际类型动态分派。这题要答好,得先知道常见的错误长什么样,再反向把正确姿势讲清。
这篇从原理讲到实战,把每一层的答法都摆出来。
两个机制的分水岭
重载看的是"参数",与返回类型、异常、访问修饰符无关。只要你换参数类型、个数或顺序,编译器就认它是另一个方法;而调用哪个,在编译期就钉死了。这也解释了为什么重载常被调侃为"编译期的多态"——其实它压根不走虚分派。
重写看的是"运行期对象的实际类型"。方法名、参数必须一致,返回类型可以是父类返回类型的子类型(协变返回),访问权限不能比父类更小,抛出的受检异常范围不能比父类更宽。两同两小一大,是重写规则的口诀。重写的方法走虚方法表,父类引用指向子类对象时,调到的就是子类那份实现。
一个极易踩的灰色地带:private、static、final 方法不参与动态派发。对静态方法的"同名写法"是隐藏而非重写;对 private 方法的同名写法因不可见,实际是新建方法。指望靠重写去覆盖它们,是很多诡异 bug 的源头。
这些坑的正确绕法
最常见的坑是把"参数类型写错"当成重写,结果方法根本没覆盖父类,多态调用悄悄走到父类实现,行为诡异地不对,排查时却怎么看都像"明明重写了"。正确做法:重写一律加 @Override 注解,编译器会替你校验签名是否真的匹配父类,手误立刻现形。
其次是重载遇上自动装箱导致的决议歧义。典型如 List 既有 remove(int index) 又有 remove(Object o),当你传 list.remove(1) 时删除的是下标 1 的元素,而不是"值为 1 的对象"——若想删对象,必须写 list.remove(Integer.valueOf(1))。这种由装箱触发的重载歧义,线上出过不少看不懂的 bug。
还有一个更隐蔽的坑:子类重写时试图收窄访问权限(比如父类是 public,子类写成 protected),编译直接报错;有人想用"缩小异常范围"之类偏方绕过,本质还是没吃透两同两小一大。把规则记死,比每次靠编译报错提醒更省心。
一段代码看懂差异
看一段能直接跑的代码,把上面的机制落到具体写法上:
class Calc {
int add(int a, int b) {
return a + b; }
double add(double a, double b) {
return a + b; } // 重载:编译期按签名决议
}
class SciCalc extends Calc {
@Override int add(int a, int b) {
return a + b + 1; } // 重写:运行期分派
}
Calc c = new SciCalc();
c.add(1, 2); // 运行期动态绑定,调子类实现,返回 4
c.add(1.0, 2.0); // 重载决议:匹配 double 版本,返回 3.0
这段代码值得盯三处:第一处,add(int,int) 与 add(double,double) 是重载,编译期就定好走哪条;第二处,SciCalc 用 @Override 真实重写了 int 版本;第三处,父类引用 c 指向子类对象,调 int 版本时运行期分派到子类,返回 4——这就是动态绑定的直观证据。面试讲到这一层,基本就稳了。
这题在面试里怎么问、怎么答
"请简单介绍一下重载与重写,它在 Android 开发中起什么作用?"先用一句话定位,再补真实场景(自定义 View 的多个构造器是重载、BaseAdapter 里重写 getView 是重写),最后落到一个细节(两同两小一大 / @Override 防手误)。干净利落。
"重载与重写的底层原理是什么?"期待的是动机、机制、代价三层,而不是一句定义了事。机制层讲清重载是编译期静态决议、重写是运行期虚方法表动态分派;代价层讲清 private/static/final 不参与重写、自动装箱让重载决议有歧义。聊到这儿画图或写代码,比形容词有说服力。
"使用重载与重写时遇到过什么问题?"行为是实战题,用真实项目回答。比如曾因少写 @Override,把方法名拼错却没覆盖父类,导致某个逻辑持续走旧实现,后来统一加注解并在 review 时校验,这类问题再没出现过。经历按场景、任务、行动、结果一条线讲,逻辑自然清晰。
"重载与重写和替代方案相比有什么优劣?"维度照例列齐:性能、接入成本、生态、团队熟悉度;结尾必须落到明确选型建议。比如要扩展行为,重写比换类型更贴合多态;要提供多种入参形态,重载比堆 if 分支更清爽。把"子类重写收窄访问权限编译报错""自动装箱致重载歧义"这类代价摆到台面上,再决定怎么用。
重写还有两个进阶考点,答出来就是加分项。其一是协变返回类型:子类重写的方法,返回类型可以是父类方法返回类型的子类型,比如父类返回 Number、子类返回 Integer,这在泛型和集合场景里天天见,既保持多态又收窄了返回类型。其二是桥接方法(bridge method):当父类用了泛型、子类重写时指定了具体类型,编译器会在子类里悄悄生成一个参数类型仍是擦除后 Object 的桥方法,用来满足"签名一致才叫重写"的字节码契约,你自己写的那个带具体类型的方法则被它转发调用——看字节码时看到这种 bridge 标记,就知道背后是类型擦除与重写的协作。
泛型还会反过来给重载挖坑。由于类型擦除,两个方法若只差在泛型参数(例如 void f(List<String>) 与 void f(List<Integer>)),擦除后签名完全相同,编译器直接拒绝它们并存,这不是重载能解决的,得靠换方法名或换参数类型来化解。把"协变返回、桥接方法、泛型擦除让重载失效"这三点讲清楚,重载与重写这题就从"会用"跨到了"懂原理"。
重写还和"契约"紧密绑定,最典型的就是 equals 与 hashCode:重写 equals 时为什么必须重写 hashCode?因为约定是"相等的对象必须有相等的哈希码",否则对象放进 HashMap 后就找不回来。这道题表面上考集合,实则考你对"重写改变了行为契约、配套方法必须同步改写"的理解。接口 default 方法的出现也让重写多了一种情形:实现类可以重写 default 方法给出自己的实现,调用时同样按实际类型动态分派,和重写在语义上完全一致。
重载决议本身也有优先级讲究。当存在定长重载和可变参数(varargs)重载都能匹配时,编译器优先选定长版本,可变参数是"兜底"选项,只在没有更贴合的签名时才启用。理解"定长优先于变长、精确类型优先于需要装箱/拆箱的版本",就能预判一堆重载方法里到底会落到哪一个,而不是靠编译器报错才后知后觉。把协变返回、桥接方法、泛型擦除、default 重写、varargs 优先级这五点收齐,重载与重写这题几乎没有盲区。
最后给你几条可落地的建议
纸上读重载与重写容易忘,写一次、复盘一次,理解才落袋。面试中关于这题,关键是从原理、应用、踩坑三个层面给出有深度的回答。
再补一条工程经验:写一组代码对比"父类引用调用被隐藏的 static 方法"与"被重写的实例方法",看输出差异——能把隐藏和重写彻底分清的人,这题基本拿满分。
复习时别孤立刷题:异常体系 Throwable、Checked 与 Unchecked 的边界这些和重写规则纠缠在一起,提前分清各自地盘,下一题才不会乱。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:final-的四种用法:变量、方法、类与参数
下一篇预告:抽象类与接口:到底该怎么选才不丢分
有任何问题欢迎在评论区留言交流。