先從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重載是靜態多分派的原因是動態分派是單分派,不關心方法參數。