在Java里方法调用是如何绑定的_Java静态与动态绑定解析

静态绑定在编译期确定调用目标,基于声明类型,适用于static、private、构造器和final方法;动态绑定在运行时根据实际类型选择方法,仅适用于非static、非private、非final的重写实例方法。

静态绑定发生在编译期,只看声明类型

Java 中的 static 方法、private 方法、构造方法和 final 方法,全部在编译时就确定了调用目标——也就是“绑”在了变量的**声明类型**上,跟实际运行时对象是谁无关。

常见错误现象:Parent p = new Child(); p.staticMethod(); 看起来像多态,但其实调用的是 Parent.staticMethod(),哪怕 Child 里重写了同签名的

static 方法,也不会被触发。

  • 静态绑定不依赖对象实例,甚至可以写成 Parent.staticMethod()(推荐写法)
  • private 方法隐式 final,子类里同名方法只是新定义,不是重写
  • 如果父类声明类型没有某个 public 实例方法,即使子类有,p.method() 也会编译报错

动态绑定依赖运行时的实际类型

只有满足「非静态、非私有、非 final」的实例方法,才会走动态绑定。JVM 在运行时根据对象的 实际类型(即 new 出来的那个类),查该类的方法表(vtable),找到最终执行的方法版本。

典型场景:Animal a = new Dog(); a.speak(); 调用的是 Dog.speak(),不是 Animal.speak()

  • 动态绑定的前提是:方法在父类中已存在(至少是声明过),且子类做了重写(@Override
  • 子类新增而父类没有的方法,无法通过父类引用调用,编译直接失败
  • 字段(field)不参与动态绑定,a.name 永远取 Animal 类里的 name,哪怕 Dog 也定义了同名字段

从字节码看绑定差异

编译后,静态绑定的方法调用指令是 invokestatic,动态绑定的是 invokevirtual。你可以用 javap -c 查看:

public class Test {
    static void s() {}
    void v() {}
    public static void main(String[] args) {
        Test t = new Test();
        t.s(); // → invokestatic
        t.v(); // → invokevirtual
    }
}

关键点在于:invokevirtual 指令本身不指定具体实现类,它靠栈顶对象的实际类型 + 方法签名去查表;而 invokestatic 直接锁定类和方法符号。

  • invokespecial 用于构造器、privatesuper.xxx(),也是静态绑定语义
  • invokeinterface 表面像动态,但本质仍是运行时查表,只是接口方法表结构不同

容易被忽略的陷阱:重载 vs 重写

重载(overload)是静态绑定,重写(override)才是动态绑定。很多人误以为“参数不同就自动多态”,其实不然。

例如:

class A { void m(Object o) { System.out.println("A-Object"); } }
class B extends A { void m(String s) { System.out.println("B-String"); } }

A a = new B();
a.m("hello"); // 编译报错!因为 A 里没有 m(String)

这里 B.m(String) 是对 A 的重载,不是重写,所以父类引用看不到它。

  • 重载解析完全在编译期完成,依据是:变量声明类型 + 实参的编译期类型
  • 重写必须方法签名(含返回类型协变)完全一致,且访问权限不能更严格
  • 泛型擦除后,ListList 的方法签名一样,无法构成重载
动态绑定真正起作用的地方,永远只在「父类引用指向子类实例 + 调用被重写的方法」这一窄缝里。其他看似相似的写法,大概率落在静态绑定或编译错误区域。