动态绑定
你一定见过这段代码:
Parent obj = new Child();
obj.method(); // 输出什么?答案是 Child 的 method。但你有没有想过:编译器怎么知道 obj 实际上是 Child,而不是 Parent?
编译时只知道 obj 的类型是 Parent,但 JVM 运行到这里,能正确找到 Child.method() 并执行它。这不是魔法,背后是一套机制——动态绑定。
绑定是什么
「绑定」说的是:一个方法调用,到底对应哪段代码。
这个对应关系在什么时候确定,决定了是静态绑定还是动态绑定。
| 静态绑定 | 动态绑定 | |
|---|---|---|
| 确定时机 | 编译时 | 运行时 |
| 对应场景 | 方法重载 | 方法重写 |
| 决定依据 | 编译器看引用类型 | JVM 看实际对象类型 |
静态绑定:编译器说了算
方法重载在编译时就决定了。编译器根据参数的类型和数量选一个最匹配的版本,不需要等到运行时。
public class StaticBindingDemo {
static class Parent {
// 两个重载方法
void process(int x) {
System.out.println("处理整数:" + x);
}
void process(double x) {
System.out.println("处理小数:" + x);
}
}
public static void main(String[] args) {
Parent obj = new Parent();
// 编译时就决定了:整数调用 process(int)
obj.process(42); // 处理整数:42
// 编译时就决定了:小数调用 process(double)
obj.process(3.14); // 处理小数:3.14
}
}编译器看到 42 是 int 类型,直接把调用编译成 process(int) 的字节码。JVM 根本不需要参与选择。
动态绑定:运行时才知道
方法重写就不一样了。编译时根本不知道 obj 实际指向的是哪个子类,只有运行时看了对象头才知道。
public class DynamicBindingDemo {
static class Parent {
void execute() {
System.out.println("Parent 执行了");
}
}
static class Child extends Parent {
@Override
void execute() {
System.out.println("Child 执行了");
}
}
public static void main(String[] args) {
Parent obj = new Child();
// 编译时:编译器只知道 obj 是 Parent 类型
// 运行时:JVM 发现 obj 实际指向 Child,调用 Child.execute()
obj.execute(); // 输出: Child 执行了
}
}这里的关键:obj.execute() 这行代码本身在编译后是一样的字节码,但 JVM 执行时走的是完全不同的路径。
JVM 怎么做到的
动态绑定的实现依赖方法分派机制。当 JVM 调用一个被重写的方法时,大致经历了以下步骤:
obj.execute()
↓
查看 obj 的实际对象类型(不是引用类型)
↓
在对象头的类指针中找到真实类型:Child
↓
在 Child 的方法表里找到 execute() 的入口地址
↓
跳转到 Child.execute() 执行这个过程发生在每次方法调用时,是多态的核心支撑。代价是轻微的性能开销——但 JIT 编译器会做内联优化,大部分情况下可以忽略不计。
重写 vs 重载:两种绑定的对比
把两种场景放在一起看,更清晰:
public class BindingComparison {
static class Base {
// 重载:参数不同,是另一个方法 → 静态绑定
void process(int x) {
System.out.println("int: " + x);
}
void process(double x) {
System.out.println("double: " + x);
}
// 重写:签名相同 → 动态绑定
void execute() {
System.out.println("Base execute");
}
}
static class Derived extends Base {
@Override
void execute() {
System.out.println("Derived execute");
}
}
public static void main(String[] args) {
Derived obj = new Derived();
// 重载 → 编译器选静态绑定
obj.process(1); // int: 1
obj.process(1.0); // double: 1.0
// 重写 → JVM 选动态绑定
obj.execute(); // Derived execute
}
}属性没有多态
有一个细节容易忽略:成员变量(属性)不存在动态绑定。属性访问在编译时就确定了。
public class FieldBindingDemo {
static class Animal {
String name = "Animal";
void speak() {
System.out.println(name); // 这里访问的是谁的值?
}
}
static class Cat extends Animal {
String name = "Cat";
@Override
void speak() {
System.out.println(name); // 这里访问的是谁的值?
}
}
public static void main(String[] args) {
Animal animal = new Cat();
// 方法是多态的(动态绑定)
animal.speak(); // 输出: Cat
// 但属性不是:编译时就按引用类型决定了
System.out.println(animal.name); // 输出: Animal
}
}这里 animal.speak() 打印的是 Cat,因为 speak() 是被重写的方法。但 animal.name 打印的是 Animal——因为属性访问是静态绑定的,编译器看到 animal 是 Animal 类型,就用了 Animal.name。
这也是为什么实际项目中属性很少用 protected,通常都 private 配合 getter 使用——避免被子类属性「覆盖」造成歧义。
总结
方法重载 → 编译时绑定 → 叫哪个方法,编译器定
方法重写 → 运行时绑定 → 叫哪个方法,JVM 看对象定
属性访问 → 编译时绑定 → 用哪个属性,编译器按引用类型定理解了绑定机制,就理解了整个多态的底层逻辑:多态的优雅,来自动态绑定的支撑。
