面试考察点

  • 能否区分编译期多态和运行期多态。
  • 是否掌握方法签名、返回类型和访问级别约束。
  • 是否知道静态方法与私有方法不参与动态重写。

核心答案

重载发生在同一个类型体系中,方法名相同但参数列表不同,编译器根据引用的静态类型和实参选择方法;重写发生在父子类之间,子类提供相同签名的实例方法,运行期根据对象实际类型动态分派。

仅修改返回类型不能构成重载。重写可以使用协变返回类型,访问权限不能更严格,抛出的受检异常范围不能比父方法更宽。

关键机制

重载候选可能涉及精确匹配、基本类型提升、装箱和可变参数,复杂组合容易产生歧义。重写依赖虚方法调用;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 传给多个重载方法会怎样?

编译器选择更具体的参数类型;若两个候选互不为子类型,例如 StringInteger,调用会产生编译歧义,需要显式强转。

追问:构造方法可以重载或重写吗?

构造器可以通过不同参数列表重载,但不会被继承,因此不能重写。子类构造器只是在第一步显式或隐式调用父类构造器。

追问: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」、配置实验和事故数据,比复述固定模板更有说服力。