标签:
虚拟机每次方法的调用和返回都伴随着栈帧的入栈和出栈,而每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用(表明该栈帧执行的是哪个方法),持有这个引用是为了支持方法调用中的动态连接。这些符号引用中一部分会在类加载阶段或者第一次使用的时候转换成直接引用(即在验证、准备、解析的解析阶段,比如说类中的静态方法、私有方法以及final修饰的方法,在编译期就可以明确调用的版本,而不会产生歧义),这种转换称为静态解析;另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
方法返回包含正常完成出口和异常完成出口。无论哪种方式返回,在方法退出后,要返回到方法被调用的位置,程序才可以继续执行,返回时,栈帧中会保存一些信息,以帮助恢复调用后状态。
正常完成出口是执行引擎遇到任意一个方法返回字节码指令,根据返回指令决定是否需要返回值以及返回值类型。
异常完成出口是方法执行中遇到异常,且异常在方法体内没有得到处理(虚拟机内部异常、athrow字节码指令产生的异常等),这种方式退出时不会给上层调用者任何返回值的。
方法调用,主要是确定被调用方法的版本,即将符号引用转换为直接引用,确定具体函数的入口地址的过程。所有方法在Class文件里存储的都是一个常量池里的符号引用,不是方法在实际运行时的内存布局中的入口地址(相当于前面所说的直接引用)。方法调用有下述四种字节码指令:
a.invokestatic:调用静态方法
b.invokespecial:调用实例构造器<init>方法、私有方法和父类方法
c.invokevirtual:调用所有的虚方法
d.invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
解析,解析的前提是方法在程序真正运行之前就有一个可确定的调用版本,而且这个方法的调用版本是运行期不可改变的。换言之,调用目标代码在代码写好、编译器进行编译时就必须确定下来。要解析的方法一般具有“编译期可知,运行期不变”的特征,而满足这个要求的方法主要由静态方法(同类型关联)和私有方法(外部不可访问)两类,他们不能通过继承或别的方式重写出其他的版本。但凡被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器和父类方法四类,他们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法被称为非虚方法,其它方法被称为虚方法(final方法除外,final方法不可被覆盖,没有其他版本,无需进行多态选择)。
public class Test{ public static void test(){ System.out.println("Hello world"); } public static void main(String[] args){ Test.test(); } }
上述代码就很容易确定方法调用结果,这个符号引用就是在编译期换为直接引用的。
解析调用是一个静态过程,编译期可完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。
分派(动态分派和静态分派),分派调用可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派,组合起来就构成了静态单分派、静态多分派、动态单分派和动态多分派。
public class Test5 { static abstract class Human{ } static class Man extends Human{ } static class Women extends Human{ } public void sayHello(Human guy){ System.out.println("Hello,guy"); } public void sayHello(Women guy){ System.out.println("Hello,lady"); } public void sayHello(Man guy){ System.out.println("Hello,gentleman"); } public static void main(String[] args) { Human man = new Man(); Human women = new Women(); Test5 t = new Test5(); t.sayHello(man); t.sayHello(women); } }
上述代码为方法重载的一个例子,main中类型Human称为变量man的静态类型,Man为实际类型,静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的;实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是啥。在main中两次sayHello调用,使用哪个版本完全取决于传入参数的数量和类型。虽然代码中参数的静态类型和实际类型不一致,但是虚拟机(编译期)重载时是通过参数的静态类型而不是实际类型作为判定依据的。且静态类型编译期可知。
所有依赖静态类型来定位方法执行版本的分派动作都称为静态分派。典型的应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
动态分派与方法的重写有紧密联系,见代码:
package com.testone; public class Test5 { static abstract class Human{ protected abstract void sayHello(); } static class Man extends Human{ @Override protected void sayHello() { // TODO Auto-generated method stub System.out.println("man say hello"); } } static class Women extends Human{ @Override protected void sayHello() { // TODO Auto-generated method stub System.out.println("women say hello"); } } public static void main(String[] args) { Human man = new Man(); Human women = new Women(); man.sayHello(); women.sayHello(); man = new Women(); man.sayHello(); } }
sayHello是所要执行的方法,sayHello方法属于实例方法,在调用时虚拟机会将方法所属对象作为参数传入,此时会根据该对象的实际类型C在类型C的定义中查找匹配sayHello的方法,并进行访问权限校验,通过则返回这个方法的直接引用,查找结束;不通过则返回IllegalAccessError异常。否则,按继承关系沿C的父类中进行搜索和验证,如果最终都没有找到合适的方法,则抛出异常。
动态分派中第一步就是在运行期确定接收者的实际类型,因而运行方法时,根据方法声明在各自对象实际类型中进行匹配,从而实现动态的分派,使得结果表现为多态的。
单分派与多分派:方法接收者和方法的参数统称为方法的宗量,单分派为根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。
package com.testone; public class Test5 { static class QQ{ } static class _360{ } public static class Father{ public void hardChoice(QQ arg){ System.out.println("father choose qq"); } public void hardChoice(_360 arg){ System.out.println("father choose 360"); } } public static class Son extends Father{ @Override public void hardChoice(QQ arg) { // TODO Auto-generated method stub System.out.println("son choose qq"); } @Override public void hardChoice(_360 arg) { // TODO Auto-generated method stub System.out.println("son choose 360"); } } public static void main(String[] args) { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); } }
首先看main中第一句加粗的语句,实际类型和静态类型相同,但是内部方法进行了重载,这个采用静态分派,在静态分派的过程中需要根据静态类型和参数类型确定调用的方法,是根据两个宗量进行选择,因而属于多分派,所以java语言的静态分派属于多分派类型。
再看第二句加粗的语句,因为实际类型和静态类型不同,且覆盖了父类方法,这个采用动态分派,在动态分派的过程中,因为方法签名已经确定,所以方法的参数类型及个数已经可以确定(此时参数的静态类型和实际类型都不会对于方法的选择构成影响),但是调用哪个对象的方法是无法确定的,这会影响方法的选择,唯一可以影响虚拟机选择的因素只有此方法的接收者的实际类型,故而只有一个宗量作为选择的依据,所以java语言的动态分派属于单分派类型。
标签:
原文地址:http://www.cnblogs.com/lxk2010012997/p/5878174.html