第014篇 重载与重写:编译期与运行期的分野

简介: 重载与重写是Java多态的两大基石:重载发生于同一类,编译期按参数列表静态绑定;重写发生于父子类,运行期按实际类型动态分派。二者易混淆,但本质分属编译期与运行期,规则、边界与典型坑(如忘加`@Override`、static隐藏、泛型擦除冲突)须清晰辨析。

重载与重写的深浅,从聊坑开始见分晓。这俩名字像,却被绑定在编译期和运行期两个完全不同的时刻,背资料的人在这里往往张冠李戴。默认行为是什么、边界在哪、出错时是什么表现,把它们讲清楚,这题的分量就出来了。

先把结论放在前面:重载(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-的四种用法:变量、方法、类与参数

下一篇预告:抽象类与接口:到底该怎么选才不丢分

有任何问题欢迎在评论区留言交流。

相关文章
|
18小时前
|
定位技术 Android开发 C++
第140篇滑动冲突解决:外部拦截与内部拦截
滑动冲突本质是多View争抢同种手势语义。解法唯二:一方主动放弃(拦截/豁免)或移交滚动权(嵌套滚动)。关键在按容器关系三类施策——父子同类用嵌套滚动,父子异类用方向锁定拦截,兄弟重叠靠层级与仲裁。
20 0
|
2天前
|
安全 编译器 API
第116篇 Kotlin 代码评审清单:团队规范与静态检查
本节聚焦Kotlin团队代码质量保障机制,提出“三层金字塔”模型:①工具层(ktlint/detekt/编译器)自动化查格式与确定性缺陷;②约定层(PR模板/基线/baseline)半强制落地规范;③设计层(架构/抽象/命名)依赖人工判断。核心是“机器管规则,人管设计”,避免告警泛滥与评审失焦。
23 0
|
2天前
|
安全 Java Android开发
第104篇 Kotlin 惯用法精选:一行替代十行 Java
本文精讲Kotlin五大高阶惯用法:空安全流、集合流水线、作用域函数选择、解构与数据类、契约式前置条件。聚焦面试真题——“如何写得更Kotlin”,强调每种惯用法的**适用场景**与**禁用时机**,避免为链而链。附机制解析、工程反例、调试避坑及团队落地规范。
18 0
|
2天前
|
缓存 安全 Android开发
第065篇 Elvis 运算符与 let:空值处理的组合拳
Kotlin 中 `?:`(Elvis)与 `let` 是被低估却极富表现力的空安全利器:`?:` 是懒求值表达式,专用于优雅兜底或提前返回;`let` 是内联作用域函数,在非空时以参数形式安全处理值。二者常组合为 `x?.let { ... } ?: default`,将“有则处理、无则兜底”升华为可组合、可读、零开销的声明式逻辑。关键在理解求值时机与意图选型——缺失用 `?:`,非空处理用 `let`,状态语义复杂时请用 sealed class 显式建模。
17 0
|
2天前
|
存储 缓存 大数据
第123篇状态保存与恢复:onSaveInstanceState 的时机
本文深入解析Android状态保存与恢复的核心逻辑:关键不在“存”,而在“判”——明确区分视图状态(View)、界面状态(Activity Bundle)与业务数据(ViewModel/Repository)三层,严守1MB Binder事务限制,规避TransactionTooLargeException;强调onSaveInstanceState不保证调用,关键状态须冗余存储;提供可落地的工程实践与面试高分要点。
25 1
|
2天前
|
缓存 安全 Java
第060篇 Java 阶段面试通关地图:六十个考点串联复盘
“Java阶段面试”不是考知识点记忆,而是考察机制理解、场景落地与工程权衡。本文以五条主线(集合、并发、JVM、异常、新特性)为地图,强调“结论+机制+场景+代价”四层回答法,直击HashMap树化、线程池调优、Android-JVM差异等高频深水区,助你从背题党蜕变为真懂原理的工程师。
34 0
|
2天前
|
传感器 安全 Android开发
第096篇 协程与生命周期:repeatOnLifecycle 的正确用法
本文深入解析 Android 协程与生命周期对齐的核心难题:明确区分 `lifecycleScope`(绑 Fragment)与 `viewLifecycleOwner.lifecycleScope`(绑 View),强调“碰 View 必用后者”这一关键判据;详解 `repeatOnLifecycle` 的启停机制与冷流重启代价,并给出 BaseFragment 模板、热流优化、Binding 安全置空等工程落地方案。
19 0
|
2天前
|
安全 Java Android开发
第055篇 Optional 与空值处理:比判空更优雅的表达
`Optional&lt;T&gt;` 是 Java 8 引入的容器类,核心价值是**将“可能为空”显式表达在类型中**,提升空安全与可读性。仅限用作**方法返回值**,禁用于字段、参数及高频路径;需搭配 `ofNullable`、`map`/`flatMap`、`orElseGet` 等规范使用,避免序列化、性能与语义陷阱。
26 0
|
2天前
|
缓存 Java API
第050篇 IO 流体系:字节流与字符流的分层设计
Java IO流是面试分水岭:字节流处理二进制,字符流处理文本(内部仍转字节);装饰器模式实现缓冲、数据/对象序列化等能力。核心考点:缓冲区优化原理、字符编码陷阱(必须显式指定UTF-8)、try-with-resources资源管理、批量读写避坑。真实项目中,用错流类型、忽略编码、未关闭流是高频故障源。
26 0
|
2天前
|
安全 编译器 测试技术
第091篇 Job 与 SupervisorJob:异常传播的分水岭
本文详解 Kotlin 协程中 `Job` 与 `SupervisorJob` 的核心差异:普通 `Job` 实现失败级联(子失败→父取消→兄弟全停),适用于“全有或全无”场景;`SupervisorJob` 切断级联,子失败仅限自身,需手动兜底异常,适合并行独立任务(如首页三路数据加载)。附机制原理、避坑指南与面试高频问答。
16 0

热门文章

最新文章