java 编译器static_从本质上,认识Java中的重载和重写

2c375a15b4926549c20bed65083a9055.png

有了坚定的意志,就等于给双脚添了一对翅膀。 —— 贝利

引导语

方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),Class文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。全面了解JVM可以在卫新搜:HaveOne

1 解析

所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析。

5条方法调用指令:

·invokestatic。用于调用静态方法。

·invokespecial。用于调用实例构造器()方法、私有方法和父类中的方法。

·invokevirtual。用于调用所有的虚方法。

·invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。

·invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”,与之相反,其他方法就被称为“虚方法”。

静态方法解析示例:

8a342d7a6dde0a7c2166a7318a936d08.png

解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。

另一种主要的方法调用形式:分派调用,可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。

分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在Java虚拟机之中是如何实现的。

2 静态分派

所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

为了解释静态分派和重载,这里先看下示例。

7c09f61173df07382bc57671773bca1b.png

为什么虚拟机会选择执行参数类型为Human的重载版本呢?

在回答问题之前,先了解几个概念,Human hman = new Man();

其中,Human是变量的“静态类型”(或外观类型),Man是变量的“实际类型”(或运行时类型)。

静态类型和实际类型在程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。(结合下图理解)

858badbd2016da2a71d6f191392e818e.png

正面回答:

main()里面的两次sayHello()方法调用,在方法接收者已经确定是对象“sds”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中故意定义了两个静态类型相同,而实际类型不同的变量,但虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。由于静态类型在编译期可知,所以在编译阶段,Javac编译器就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。

3 动态分派

Java语言里动态分派的实现过程,它与Java语言多态性的另外一个重要体现。

同样看下示例:方法动态分派

3b8ab38900332692534f721258c7e0c3.png

Java虚拟机是如何判断应该调用哪个方法的?

导致这个现象的原因很明显,是因为这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?

我们使用javap命令输出这段代码的字节码,尝试从中寻找答案。

5211c8eb6352b74246ba2596d5462f8d.png

根据《Java虚拟机规范》,invokevirtual指令的运行时解析过程[插图]大致分为以下几步:

1) 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。

2 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。

3) 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。

4) 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

总之,如今Java语言是一门静态多分派、动态单分派的语言。

4 扩展

字段没有多态性,运行下面代码观察运行结果

717ddc1cf64d90dbc8fb129e113a5727.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值