重载、重写与动态绑定
重载在编译阶段选择方法签名,重写让子类替换继承得到的实例方法实现,动态绑定则在运行时根据实际对象找到最终执行的方法。一次方法调用可能同时经过这三个环节。
1. 重载在编译阶段选择方法签名
同一个类中,方法名相同但参数列表不同,就形成重载:
final class Formatter {
String format(int value) {
return Integer.toString(value);
}
String format(long value) {
return Long.toString(value);
}
String format(Object value) {
return String.valueOf(value);
}
}
编译器根据方法名、参数数量、参数的编译时类型以及允许的方法调用转换,确定本次调用对应哪一个签名:
Formatter formatter = new Formatter();
formatter.format(1); // format(int)
formatter.format(1L); // format(long)
Object value = 1;
formatter.format(value); // format(Object)
最后一行中,对象的运行时类型是 Integer,变量的编译时类型却是 Object。重载解析只使用编译阶段可见的信息,因此选择 format(Object)。
1.1 哪些差异可以形成重载
重载方法的参数列表必须不同,常见方式包括:
- 参数数量不同。
- 对应位置的参数类型不同。
- 参数顺序不同,但这种重载通常不易阅读。
只有返回类型不同,不能形成重载:
// 编译错误:两个方法具有相同的签名
// int parse(String value) { ... }
// long parse(String value) { ... }
调用方可以忽略返回值,也可能把结果用于不同上下文。Java 不使用期望的返回类型来区分这两个声明。
1.2 重载可能产生歧义
static void print(String value) {}
static void print(Integer value) {}
// print(null); // 编译错误:两个方法都适用,且没有一个更具体
null 可以转换为 String 或 Integer,两个类型之间又没有子类型关系,编译器无法选择。重载数量越多,自动装箱、基本类型提升、可变参数和泛型参与后,调用规则越难直观看出。
设计 API 时,语义不同的操作优先使用不同方法名,不要只为了减少名字而建立复杂重载。
2. 重写替换继承的方法实现
子类声明与父类实例方法具有相同签名的方法,可以重写父类实现:
class MessageSender {
public void send(String message) {
System.out.println("send: " + message);
}
}
final class RetryableMessageSender extends MessageSender {
@Override
public void send(String message) {
System.out.println("send with retry: " + message);
}
}
常用的重写规则包括:
- 方法名和参数类型必须与被重写方法一致。
- 返回类型可以相同,也可以是原返回类型的子类型。
- 访问权限不能比父类方法更严格。
- 受检异常的范围不能比父类方法更宽。
final方法不能重写,private方法不会被继承,static同名方法属于隐藏。
始终为准备重写的方法添加 @Override。如果参数写错导致方法实际变成重载,编译器会直接报告问题。
final class Account {
private final long id;
Account(long id) {
this.id = id;
}
// @Override 会发现 equals(Account) 没有重写 equals(Object)
// public boolean equals(Account other) { ... }
@Override
public boolean equals(Object other) {
return other instanceof Account account && id == account.id;
}
}
3. 一次调用分为编译选择和运行时查找
下面的代码同时包含重载和重写:
class Printer {
public void print(Object value) {
System.out.println("object: " + value);
}
}
final class JsonPrinter extends Printer {
@Override
public void print(Object value) {
System.out.println("json object: " + value);
}
public void print(String value) {
System.out.println("json string: " + value);
}
}
Printer printer = new JsonPrinter();
printer.print("Coding 101");
结果是:
json object: Coding 101
可以分两步判断:
- 编译阶段选择签名:变量
printer的编译时类型是Printer,可见的候选方法只有print(Object),所以调用签名已经确定。 - 运行时选择实现:实际对象是
JsonPrinter,它重写了print(Object),动态查找最终执行这个实现。
JsonPrinter.print(String) 虽然参数更具体,但它不属于编译时类型 Printer 的成员集合,因此不会参与这次重载解析。
如果变量直接声明为 JsonPrinter,结果会不同:
JsonPrinter jsonPrinter = new JsonPrinter();
jsonPrinter.print("Coding 101"); // json string: Coding 101
此时两个重载都可见,编译器选择参数更具体的 print(String)。
先使用引用的编译时类型和参数的编译时类型确定方法签名,再判断该签名对应的实例方法是否会根据运行时对象动态绑定。
4. 动态绑定适用于哪些调用
通常的可重写实例方法会动态绑定。JVM 从实际对象的类开始查找与已选签名对应的方法实现。
下面几类成员不按这种方式选择:
4.1 静态方法根据编译时类型选择
class Parent {
static String name() {
return "parent";
}
}
class Child extends Parent {
static String name() {
return "child";
}
}
Parent value = new Child();
System.out.println(value.name()); // parent
静态方法属于类。虽然 Java 允许通过变量调用它,但使用类名 Parent.name() 更能直接表达选择结果。
4.2 private 方法不会被子类重写
父类的 private 方法不被子类继承。子类可以声明一个同名方法,但它是独立的新方法,不会替换父类内部对私有方法的调用。
4.3 final 方法不允许重写
final 固定了继承层次中的方法实现。它仍然是实例方法,但编译器已经保证子类不能提供替代实现。
4.4 字段访问不是方法调用
子类声明同名字段属于隐藏,字段选择由引用的编译时类型决定。需要按照对象类型变化的行为,应当放在可重写的实例方法中。
5. 常见问题
5.1 构造方法可以重载或重写吗
构造方法可以重载,因为同一个类可以提供多组不同参数的构造方法。构造方法不会被继承,因此不能重写。子类构造方法只是通过 super(...) 调用父类构造方法。
5.2 可变参数与数组参数能否形成重载
不能。String... values 在方法签名中按照 String[] 处理,下面两个声明会发生冲突:
// void join(String... values) {}
// void join(String[] values) {}
5.3 为什么重写方法可以缩小返回类型
如果父类方法返回 Animal,子类方法返回 Cat,所有原本接受 Animal 的调用方仍然可以使用结果。这个规则称为协变返回类型。基本类型没有子类型关系,因此不能把 int 改成 long。
5.4 @Override 是不是只起说明作用
不是。编译器会检查被标注的方法是否重写或实现了父类型方法。它能够捕获参数写错、方法名写错等问题,避免本来准备重写的方法悄悄变成一个新的重载。
6. 面试题
6.1 重载和重写有什么区别
出现公司:招银网络科技、慧策、有赞
考察重点
- 两者发生在同一个类还是继承关系中。
- 方法签名、返回类型、访问权限和异常规则。
- 编译阶段重载解析与运行时动态绑定的区别。
相关内容:第 1 节“重载在编译阶段选择方法签名”、第 2 节“重写替换继承的方法实现”、第 3 节“一次调用分为编译选择和运行时查找”。
参考回答
重载是同名方法使用不同参数列表,编译器根据接收者和参数的编译时类型选择一个方法签名。仅修改返回类型不能形成重载。
重写发生在继承或接口实现关系中,子类为同签名的实例方法提供新实现。返回类型可以协变,访问权限不能缩小,受检异常不能扩大。调用可重写实例方法时,签名仍在编译阶段确定,JVM 再根据对象的运行时类型动态选择最终实现。
6.2 父类引用指向子类对象时,方法怎样确定
出现公司:美团
考察重点
- 能否把重载解析和重写查找拆成两个阶段。
- 编译时类型与运行时类型分别影响什么。
- 静态方法、字段与实例方法的选择是否相同。
相关内容:第 3 节“一次调用分为编译选择和运行时查找”、第 4 节“动态绑定适用于哪些调用”。
参考回答
编译器先根据引用的编译时类型和参数的编译时类型确定可调用的方法,并完成重载解析。这个阶段决定方法签名。
如果选中的是可重写实例方法,运行时再从实际对象类型开始查找对应实现。静态方法和字段由编译时类型决定,private 方法不会被继承,final 方法不能被重写,因此不按普通实例方法的动态绑定规则选择。