您的位置:首页 > 编程语言 > Java开发

4段代码了解Java虚拟机虚方法和非虚方法的分派

2017-04-04 19:07 281 查看
先从2段代码聊起,

代码1:

public class SuperTest {
public static void main(String[] args) {
new Sub().exampleMethod();
}
}

class Super {
private void interestingMethod() {
System.out.println("Super's interestingMethod");
}

void exampleMethod() {
interestingMethod();
}
}

class Sub extends Super {
void interestingMethod() {
System.out.println("Sub's interestingMethod");
}
}


代码2:

public class SuperTest {
public static void main(String[] args) {
new Sub().exampleMethod();
}
}

class Super {
void interestingMethod() {
System.out.println("Super's interestingMethod");
}

void exampleMethod() {
interestingMethod();
}
}

class Sub extends Super {

void interestingMethod() {
System.out.println("Sub's interestingMethod");
}
}


两段代码唯一一处不同的地方在于代码1的父类Super中的interestingMethod()是private void方法,而代码2中父类Super的interestingMethod()方法为void方法。

那么,这两段代码的输出结果会一样吗?

第一段代码的输出

Super's interestingMethod


可以看到,第一段代码调用了父类的interestingMethod方法。

第二段代码的输出:

Sub's interestingMethod


第二段代码则调用了子类的interestingMethod方法。

为什么会这样呢?这里需要说到Java里哪些是虚方法,哪些是非虚方法?虚方法又如何分派? 除了静态方法之外,声明为final或者private的实例方法是非虚方法。其它(其他非private方法)实例方法都是虚方法。

虚方法和非虚方法的调用又有什么区别呢?在Java 虚拟机里面提供了5条方法调用字节码指令,分别如下:

invokestatic:调用静态方法

invokespecial:调用实例构造器方法,私有方法和父类方法等非虚方法

invokevirtual:调用所有的虚方法

invokeinterface:调用所有的接口方法

invokedynamic:动态运行解析

对非虚方法的调用,程序在编译时,就可以唯一确定一个可调用的版本,且这个方法在运行期不可改变,那么会在类加载的解析阶段,通过前面的指令1,指令2将对这个方法的符号引用转为对应的直接引用,即转为直接引用方法。在Java中,静态方法,final方法和private方法 都是不可在子类中重写的。所以他们都是非虚方法。

代码1中的非虚方法调用的指令(…表示省略了一些上下文)javap -verbose Sub

...
Constant pool:
...
#30 = Methodref          #1.#31         //  jvmbook/Super.interestingMethod:()V
...

void exampleMethod();
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #30                 // Method interestingMethod:()V
4: return
LineNumberTable:
line 16: 0
line 17: 4
LocalVariableTable:
Start  Length  Slot  Name   Signature
0       5     0  this   Ljvmbook/Super;


代码2中的虚方法调用的指令(…表示省略了一些上下文)javap -verbose Sub

...
Constant pool:
...
#30 = Methodref          #1.#31         //  jvmbook/Super.interestingMethod:()V
...

void exampleMethod();
...
1: invokevirtual #30                 // Method interestingMethod:()V
4: return
...


Super su =new Sub();
//前面的Super称为su的静态类型,后面的Sub称为su的实际类型


invokevirtual的语义是要尝试做虚方法分派,而invokespecial不尝试做虚方法分派。 即invokevirtual调用的方法需要在运行时,根据目标对象的实际类型(代码2中为sub)来动态判断需要执行哪个方法。而invokespecial则只根据常量池中对应序号是哪个方法就执行哪个方法(即看静态类型)。 这里有特殊的一点是,final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖(不存在其他版本),所以也无须对方法接收者进行多态选择,或者说多态选择的结果是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法

总结起来就是,非虚方法调用只看对象的静态类型。

那虚方法调用呢?结论是invokevirtual调用分2步,第一步在编译期先看方法调用者和参数的静态类型,第二步在运行期再看且只看方法调用者的动态类型。

代码3:

public class StaticSDispatch {
static abstract class Human {}
static class Man extends Human {}
static class Woman extends Human {}

public void sayHello(Human guy) {
System.out.println("hello,guy");
}

public void sayHello(Man man) {
System.out.println("hello,man");
}

public void sayHello(Woman woman) {
System.out.println("hello,woman");
}

public static void main(String[] args) {
Human man = new Man();
Human women = new Woman();
StaticSDispatch sd = new StaticSDispatch();
sd.sayHello(man);
sd.sayHello(women);
}
}

//输出结果
hello,guy
hello,guy


代码3的解释:

首先sayHello()方法是虚方法,通过invokevirtual指令调用。因为在编译期只看方法接收者和参数的静态类型,所以在编译完成后,产生了2条指令,选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到了main()方法里面的2条invokevirtual指令的参数中。然后在运行期,动态选择sd的实际类型,因为在这sd没有父类,所以不用考虑。

还有另外一种解释是,所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型例子是方法重载。

代码3的字节码:

public static void main(java.lang.String[]);
...
26: invokevirtual #51                 // Method sayHello:(Ljvmbook/StaticSDispatch$Human;)V
29: aload_3
30: aload_2
31: invokevirtual #51                 // Method sayHello:(Ljvmbook/StaticSDispatch$Human;)V
...
}


代码4:

public class DynamicDispatch {
static abstract class Human{
protected abstract void sayHello();
}

static class Man extends Human{
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}

static class Women extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}

public static void main(String[] args) {
DynamicDispatch dy =new DynamicDispatch();
Human man =new Man();
Human women =new Women();
man.sayHello();
women.sayHello();
man =new Women();
man.sayHello();
}
}

//输出结果
man say hello
woman say hello
woman say hello


代码4的解释:

首先,sayHello()是虚方法,所以调用指令是invokevirtual.因为该方法没有参数,且方法接收者man/women的实际类型是Human,所以在编译期完成后会产生2条指令:Human.sayHello();然后在动态运行时,只根据方法

接收者的动态类型来动态分派,即会分派Man/Women的sayHello()方法

总结:

根据4段代码总结起来就是几句话:

1.非虚方法(所有static方法+final/private 方法)通过invokespecial指令调用(final虽然是非虚方法,但是通过invokevirtual调用),不尝试做虚方法分派,对这个非虚方法的符号引用将转为对应的直接引用,即转为直接引用方法,在编译完成时就确定唯一的调用方法。

2.虚方法通过invokevirtual指令调用,且会有分派。具体先根据编译期时方法接收者和方法参数的静态类型来分派,再在运行期根据只根据方法接收者的实际类型来分派,即Java语言是静态多分派,动态单分派类型的语言。需要注意的是,在运行时,虚拟机只关心方法的实际接收者,不关心方法的参数,只根据方法接收者的实际类型来分派。

那么问题来了:

public class Dispatcher {

static class QQ {
}

static class _360 {
}

public static class Father {
public void hardChoice(QQ qq) {
System.out.println("father choose qq");
}

public void hardChoice(_360 _360) {
System.out.println("father choose 360");
}
}

public static class Son extends Father{
public void hardChoice(QQ qq) {
System.out.println("son choose qq");
}

public void hardChoice(_360 _360) {
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());

}

}


这段代码又会输出什么?

还有一点,为什么Java方法的重载是静态多分派?因为动态单分派时不关心方法的参数,只关心方法的接收者。而方法重载是方法名一样,方法参数不一样,也就导致无法做到动态分派。所以Java重载是静态多分派的原因是动态分派是单分派,不关心方法参数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java jvm