面试考察点
- 能否区分编译期多态和运行期多态。
- 是否掌握方法签名、返回类型和访问级别约束。
- 是否知道静态方法与私有方法不参与动态重写。
核心答案
重载发生在同一个类型体系中,方法名相同但参数列表不同,编译器根据引用的静态类型和实参选择方法;重写发生在父子类之间,子类提供相同签名的实例方法,运行期根据对象实际类型动态分派。
仅修改返回类型不能构成重载。重写可以使用协变返回类型,访问权限不能更严格,抛出的受检异常范围不能比父方法更宽。
关键机制
重载候选可能涉及精确匹配、基本类型提升、装箱和可变参数,复杂组合容易产生歧义。重写依赖虚方法调用;static 方法属于类,只会被隐藏,private 方法对子类不可见。
规则对比
| 维度 | 重载 Overload | 重写 Override |
|---|---|---|
| 决定时机 | 编译期 | 运行期 |
| 发生位置 | 同类或继承体系都可形成候选 | 父子类实例方法之间 |
| 参数列表 | 必须不同 | 必须相同 |
| 返回类型 | 不参与重载区分 | 相同或协变子类型 |
| 访问权限 | 独立决定 | 不能比父方法更严格 |
| 受检异常 | 独立决定 | 不能扩大父方法声明范围 |
重载决议示例
void print(long value) {}
void print(Integer value) {}
void print(int... values) {}
print(1); // int 基本类型提升为 long
编译器不会简单地“随便找一个能转的”。常见优先级可概括为精确匹配、基本类型拓宽、装箱/拆箱、可变参数,但泛型、继承和 null 会让候选更复杂。公共 API 应避免让调用者依赖微妙规则。
void handle(String value) {}
void handle(Integer value) {}
// handle(null); // 编译失败:两个候选都适用且互不更具体
动态绑定示例
class Parent {
Number value() { return 1; }
static String type() { return "parent"; }
}
class Child extends Parent {
@Override Integer value() { return 2; }
static String type() { return "child"; }
}
Parent ref = new Child();
ref.value(); // Child.value,实例方法动态分派
ref.type(); // Parent.type,静态方法按引用类型解析
协变返回让子类返回更具体的类型。编译器处理泛型重写时还可能生成桥接方法,以维持类型擦除后的多态契约;反射扫描方法时可能看到 isBridge() 为 true 的合成方法。
API 设计建议
同名重载应该具有一致含义和一致失败语义,例如 of(String) 与 of(Path) 都创建同一种值。布尔参数、相邻数字类型和 null 容易让重载难懂,复杂配置更适合建造者或不同方法名。
实践边界
重载应保持语义一致,例如不同参数形式都表示同一操作。重写始终加 @Override,让编译器发现拼写或签名错误;构造器中避免调用可重写方法,以免子类状态尚未初始化。
常见误区
字段访问没有运行时多态,取决于引用声明类型。返回值不参与普通重载决议,所以不能定义两个仅返回类型不同的方法。
高频追问与参考回答
追问:null 传给多个重载方法会怎样?
编译器选择更具体的参数类型;若两个候选互不为子类型,例如 String 与 Integer,调用会产生编译歧义,需要显式强转。
追问:构造方法可以重载或重写吗?
构造器可以通过不同参数列表重载,但不会被继承,因此不能重写。子类构造器只是在第一步显式或隐式调用父类构造器。
追问:final 方法和抽象方法有什么关系?
final 禁止子类重写,abstract 要求子类提供实现,同一方法不能同时满足两种互斥语义。private 方法也不能被子类重写。
追问:方法参数的泛型不同能构成重载吗?
method(List<String>) 与 method(List<Integer>) 擦除后签名相同,不能同时声明。泛型 API 设计必须考虑类型擦除后的 JVM 方法描述符。
总结
重载看编译期参数,重写看运行期对象;回答时补充静态方法隐藏和重写约束更完整。
机制全景图
下面把「方法重载和重写有什么区别?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。
flowchart LR
A["收集候选方法"]
A --> B["编译期选择重载"]
B --> C["必要时装箱或提升"]
C --> D["运行期选择重写实现"]
D --> E["返回结果或抛出异常"]
完整链路:从输入到结果
沿着「收集候选方法 → 编译期选择重载 → 必要时装箱或提升 → 运行期选择重写实现 → 返回结果或抛出异常」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。
1. 收集候选方法
重载候选由方法名和可见范围确定,返回类型不能单独区分两个重载。
2. 编译期选择重载
编译器依据变量的静态类型和实参类型选择最具体签名,因此重载是早绑定。
3. 必要时装箱或提升
精确匹配、基本类型提升、装箱与可变参数存在优先级,null 还可能让多个引用重载产生歧义。
4. 运行期选择重写实现
真正调用哪个重写方法由接收者运行期类型决定,因此重写是多态分派;静态方法与字段不参与这种分派。
5. 返回结果或抛出异常
子类重写不能缩小可见性,受检异常范围也不能更宽,协变返回类型则允许返回更具体类型。
源码与实现定位
| 入口 | 阅读重点 |
|---|---|
| javap invokevirtual/invokestatic | 区分运行期虚分派与静态调用 |
| java.lang.Class#getMethods | 桥接、继承与重写后的可见方法 |
源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。
参数配置与可复现实验
static void print(Object x) { }
static void print(String x) { }
Object value = "text";
print(value); // 编译期选择 print(Object)
编译包含 null、装箱、可变参数和父类静态类型的调用矩阵,用 javap 确认描述符;升级 API 前跑二进制兼容检查。
验证步骤与预期结果
1. 固定输入和基线
先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「重载候选数」为主基线,记录值应满足「同语义控制在少量」;同时保存 编译歧义与升级失败、重载数量与调用分布,使后续变化能够回到同一时间轴比较。
2. 从实现入口确认路径
在「javap invokevirtual/invokestatic」确认请求确实进入「区分运行期虚分派与静态调用」对应的实现,再沿「java.lang.Class#getMethods」观察「桥接、继承与重写后的可见方法」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。
3. 注入本文特有的失败模式
优先复现「null 在多个引用重载间产生歧义」,并把单一变量逐级放大,直到「重载候选数」越过「超过 5 个且类型接近」。随后再分别验证「误以为父类变量会选择子类特有重载」和「重写方法放宽受检异常破坏调用方契约」,三类故障分开执行,避免多个变量同时变化而无法归因。
4. 执行止损和根因修复
第一轮只应用「语义不同的重载改为明确方法名」,确认它能控制影响范围;第二轮应用「公共 API 新增重载前跑源码兼容测试」,验证核心链路恢复;最后落实「多变行为提取策略而非加继承层」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。
5. 通过退出条件
实验只有同时满足三项才算通过:「重载候选数」回到「同语义控制在少量」、「null 歧义编译数」回到「目标为 0」、「重写契约测试」回到「子类全部通过」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。
量化基线
| 指标 | 样例基线/口径 | 风险线 | 结论 |
|---|---|---|---|
| 重载候选数 | 同语义控制在少量 | 超过 5 个且类型接近 | 改不同方法名/参数对象 |
| null 歧义编译数 | 目标为 0 | 新增重载后出现 | 撤回或改变签名 |
| 重写契约测试 | 子类全部通过 | 异常/前置条件变严 | 修复里氏替换 |
这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。
事故复盘:日志工具新增重载后调用行为改变
工具类同时提供 log(Object) 与 log(String...)。某调用传入 null 后编译器无法确定更具体目标,升级即编译失败;另一些调用因静态类型为 Object 落到非预期重载。解决方案是减少语义相近的重载,并在边界使用明确类型或不同方法名。
| 失败模式 | 首要证据 | 第一处置动作 |
|---|---|---|
| null 在多个引用重载间产生歧义 | 编译歧义与升级失败 | 语义不同的重载改为明确方法名 |
| 误以为父类变量会选择子类特有重载 | 重载数量与调用分布 | 公共 API 新增重载前跑源码兼容测试 |
| 重写方法放宽受检异常破坏调用方契约 | 运行期实现类型 | 多变行为提取策略而非加继承层 |
发布与回滚检查点
- 发布前:确认「javap invokevirtual/invokestatic」对应实现和上述配置在目标版本仍然有效,并保存「重载候选数」基线。
- 灰度中:同时观察 编译歧义与升级失败、重载数量与调用分布、运行期实现类型;任一指标越过表中风险线,就停止继续扩量。
- 回滚时:先执行「语义不同的重载改为明确方法名」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
- 发布后:至少覆盖一个完整峰值周期,确认「null 在多个引用重载间产生歧义」没有再次出现,才关闭变更观察窗口。
方案对比与选型
| 方案 | 更适合的场景 | 主要收益 | 代价与边界 |
|---|---|---|---|
| 重载 | 同一动作对不同静态参数形态提供便利 | 调用自然、编译期发现 | 过多重载会产生歧义与兼容风险 |
| 重写 | 子类替换父类行为 | 支持运行期多态 | 必须遵守里氏替换与父类契约 |
| 策略对象 | 行为需要独立扩展和组合 | 避免继承层级与重载爆炸 | 调用方需显式选择或注入策略 |
选型至少带上 对象创建率、调用频次、输入规模和 API 边界,并用上面的量化基线验证;未知数据应明确为待测假设。
设计边界与工程取舍
重载解决调用便利性,重写解决多态替换性;当不同参数实际代表不同业务语义时,使用不同方法名往往比继续增加重载更清晰。
工程落地遵循:优先保证语言语义、类型契约和向后兼容,再讨论微小性能收益。回答时直接引用「javap invokevirtual/invokestatic」、配置实验和事故数据,比复述固定模板更有说服力。