电脑技术汇 9小时前
02-重载和重写在字节码层面差在哪,静态分派和动态分派讲透
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_font3.html

 

重载和重写在字节码层面差在哪,静态分派和动态分派讲透

有道题我见过无数遍,几乎成了 Java 面试的暗号:

5

static class Man extends Human {}

7

static class Woman extends Human {}

void say ( Human h ) { System.out.println ( "human" ) ; }

void say ( Man m ) { System.out.println ( "man" ) ; }

void say ( Woman w ) { System.out.println ( "woman" ) ; }

public static void main ( String [ ] args ) {

Human woman = new Woman ( ) ;

Dispatch d = new Dispatch ( ) ;

跑出来两行都是 human。很多人第一反应是「不对啊,明明 new 的是 Man 和 Woman」。答案的钥匙就俩词:静态分派管重载,动态分派管重写。这两件事发生在完全不同的阶段,一个编译期定死,一个运行期才知道。

重载:编译期就选好了,靠静态类型

say 的三个版本是重载(overload)。选哪个重载版本,编译器在编译期就决定了,它只看变量声明的类型,也就是「静态类型」。

man 的静态类型是 Human,不管它实际指向 Man 对象,编译器一律按 Human 去匹配重载,选中 say ( Human ) 。这个选择直接烧进字节码里,运行期不会再改。javap 一看就实锤:

两次调用指向的是同一个符号引用 #7,参数类型明明白白写着 Human。编译期就把「调哪个重载」定死了,这就叫静态分派——依据静态类型,在编译期完成。

这里有个面试常追问的点:重载方法匹配还有个「就近」的自动转型顺序。比如没有 say ( Human ) 只有 say ( Object ) 和 say ( Serializable ) 时,编译器会按 char → int → long → float ... → 自动装箱 → 装箱后的父类 → 变长参数 这一路往上找最贴近的那个。这些全是编译期的静态分派逻辑,跟运行时对象是什么没半点关系。

重写:运行期才知道,靠实际类型

把例子改一下,看重写(override):

1

static class Human { void greet ( ) { System.out.println ( "hello human" ) ; } }

3

static class Man extends Human { void greet ( ) { System.out.println ( "hello man" ) ; } }

这回输出 hello man。同样是 Human p,为什么重写就按实际类型走,重载就按静态类型走?

因为重写的分派发生在运行期。看字节码:

注意第 9 行,符号引用写的是 Human.greet ——编译器只能按静态类型生成符号引用,它不知道运行时是 Man 还是别的。真正的魔法在 invokevirtual 这条指令的执行语义里。

invokevirtual 执行时分几步:先从操作数栈顶取出对象引用,找到这个对象的实际类型,在实际类型里去找匹配方法描述符的方法;找到就调用,找不到就沿着继承链往父类找。p 栈顶是个 Man 对象,实际类型是 Man,Man 里有 greet,于是调 Man 的版本。这个「取实际类型 → 查方法」的过程,就是动态分派,也是多态的字节码实现。

一句话把两者的差别钉死

面试里最容易翻车的地方,是把「invokevirtual 的符号引用是 Human.greet」误读成「所以调的是 Human 的方法」。不是。符号引用只是编译期能填的最精确的东西,指令真正跑起来是按栈顶对象的实际类型解析的。这两层要分清。

一个能加分的细节:为什么重载不做成动态的

有人会问,那 JVM 能不能也把重载做成运行期按实际类型选?理论上可以,但代价很大——每次调用都要在运行期比较所有重载版本的参数类型,做最优匹配,开销高且语义容易含糊。Java 的设计是把「选重载」这种能在编译期确定的事全放到编译期,运行期只留「选重写」这一件必须动态的事。一静一动,各司其职。

还有个坑值得记:字段(field)没有多态。

3

System.out.println ( p.name ) ;

字段访问用的是 getfield 指令,它按静态类型解析,不走动态分派。所以父子类有同名字段时,用父类引用访问拿到的是父类那个。这条经常被拿来当「你到底懂不懂多态只针对方法」的验证题。

把这套逻辑理顺,那道 human / human 的经典题你就再也不会答错,还能反过来给面试官讲清楚为什么。

宙世代

宙世代

ZAKER旗下Web3.0元宇宙平台

一起剪

一起剪

ZAKER旗下免费视频剪辑工具

相关标签

有道
相关文章
评论
没有更多评论了
取消

登录后才可以发布评论哦

打开小程序可以发布评论哦

12 我来说两句…
打开 ZAKER 参与讨论