Skip to content

动态绑定

你一定见过这段代码:

java
Parent obj = new Child();
obj.method(); // 输出什么?

答案是 Child 的 method。但你有没有想过:编译器怎么知道 obj 实际上是 Child,而不是 Parent?

编译时只知道 obj 的类型是 Parent,但 JVM 运行到这里,能正确找到 Child.method() 并执行它。这不是魔法,背后是一套机制——动态绑定

绑定是什么

「绑定」说的是:一个方法调用,到底对应哪段代码

这个对应关系在什么时候确定,决定了是静态绑定还是动态绑定。

静态绑定动态绑定
确定时机编译时运行时
对应场景方法重载方法重写
决定依据编译器看引用类型JVM 看实际对象类型

静态绑定:编译器说了算

方法重载在编译时就决定了。编译器根据参数的类型和数量选一个最匹配的版本,不需要等到运行时。

java
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 实际指向的是哪个子类,只有运行时看了对象头才知道。

java
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 重载:两种绑定的对比

把两种场景放在一起看,更清晰:

java
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
    }
}

属性没有多态

有一个细节容易忽略:成员变量(属性)不存在动态绑定。属性访问在编译时就确定了。

java
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——因为属性访问是静态绑定的,编译器看到 animalAnimal 类型,就用了 Animal.name

这也是为什么实际项目中属性很少用 protected,通常都 private 配合 getter 使用——避免被子类属性「覆盖」造成歧义。

总结

方法重载  → 编译时绑定 → 叫哪个方法,编译器定
方法重写  → 运行时绑定 → 叫哪个方法,JVM 看对象定
属性访问  → 编译时绑定 → 用哪个属性,编译器按引用类型定

理解了绑定机制,就理解了整个多态的底层逻辑:多态的优雅,来自动态绑定的支撑

基于 VitePress 构建