Java方法调用的绑定取决于方法类型:实例方法(非static、非private、非final)采用动态绑定,运行时根据对象实际类型决定调用版本;static、private、final方法则为静态绑定,编译期确定。

Java中方法调用到底绑定了哪个实现?
Java多态的核心不是“能写父类引用指向子类对象”,而是运行时决定调用哪个method——这叫动态绑定。它只对instance method生效,static method、private method、final method全走静态绑定,编译期就定死了。
常见错误现象:obj.doSomething()看起来调用了子类方法,但实际执行的是父类的——八成是该方法被static或private修饰了;或者子类里重写时改了参数列表(变成重载而非重写),导致父类版本被调用。
- 必须满足:非
static、非private、非final,且子类中签名完全一致(包括返回类型协变) - 构造器里调用可重写的方法很危险:此时子类字段可能还未初始化,
this指向子类实例,但子类构造逻辑还没跑,容易读到默认值或null - 接口默认方法(
default)也参与动态绑定,但static接口方法不参与
为什么toString()、equals()总能体现多态?
因为它们是典型的、被广泛重写的instance method,且没被final锁死。只要子类覆写了,哪怕声明类型是Object,运行时也会走到子类逻辑。
使用场景:日志打印、集合比较、断言校验——这些地方你几乎从不直接写new SubClass().toString(),而是依赖多态自动派发。
立即学习“Java免费学习笔记(深入)”;
- 注意
equals()必须同时重写hashCode(),否则放进HashMap会出问题,这不是动态绑定本身的问题,但常和它一起暴露 -
toString()若没重写,会继承Object.toString(),输出类似ClassName@hash,容易误以为“没走多态”,其实是根本没覆写 - IDE自动生成的
toString()通常包含所有字段,但如果子类新增了字段却忘了更新,输出内容就会漏掉关键信息
javap -c能看出动态绑定的痕迹吗?
能,而且很直观。编译后的字节码里,普通实例方法调用指令是invokevirtual,这就是动态绑定的标记;而invokestatic、invokespecial(用于super.、构造器、private)都是静态绑定。
示例:javap -c MyClass 输出中看到invokevirtual #5,说明JVM会在运行时查虚方法表(vtable)找真正要执行的版本。
- 不要指望在字节码里看到“子类名”,
invokevirtual只存方法签名引用,具体实现由运行时对象类型决定 - 如果某个方法被
final修饰,javap可能显示invokevirtual,但JIT编译器大概率会内联优化掉虚调用,实际不查表 -
invokedynamic是给Lambda和方法句柄用的,和传统多态无关,别混淆
子类重写时抛出异常比父类更宽泛会怎样?
编译直接报错:error: overridden method does not throw <code>Exception。Java强制要求子类重写方法的throws声明不能比父类更宽——这是编译期检查,和动态绑定无关,但常被误认为是多态限制。
本质是类型安全:父类声明只抛IOException,调用方按此契约处理;如果子类偷偷抛Exception,调用方没准备捕获,就破坏了Liskov替换原则。
- 子类可以缩小异常范围(比如父类抛
Exception,子类不抛任何异常),也可以不写throws - 运行时异常(
RuntimeException及其子类)不受此限,因为无需声明,自然也不受检查 - 这个规则在接口默认方法重写时同样适用,不是仅限于抽象类
动态绑定真正难搞的地方不在语法层面,而在对象生命周期和继承层次变深时——比如中间某层父类悄悄加了个final方法,或者子类无意中把重写写成了重载,这时候调试器里看调用栈,很容易盯错目标。








