JVM(複習)方法調用
一,方法重載
何爲靜態類型,何爲實際類型?
static class GrandFather{
}
static class Father extends GrandFather{
}
static class Child extends Father{
}
上面聲明瞭三個類型:
GrandFather grandFather = new Father();
grandFather的靜態類型是GrandFather,而grandFather的實際類型是真正指向的類型是Father,變量的靜態類型是不會發生變化的,而變量的實際類型是可以發生變化的(多態),實際類型在運行期確定
所有依賴靜態類型來定位執行哪一個方法的動作就叫做靜態分派
public void test(GrandFather grandFather){
System.out.println("GrandFather");
}
public void test(Father Father){
System.out.println("Father");
}
public void test(Child Child){
System.out.println("Child");
}
//方法重載是一種靜態分派行爲,在編譯期可以確定
public static void main(String[] args) {
//
GrandFather father = new Father();
GrandFather child = new Child();
Father father1 = new Father();
Test1 test1 = new Test1();
test1.test(father); // GrandFather
test1.test(father1);// Father
test1.test(child);// GrandFather
}
所以,不難得多,根據靜態分派規則看靜態類型執行相應的重載的方法
重載方法的匹配優先級
在很多情況下,重載方法的版本並不是唯一,選擇調用的那個重載方法只是當前情況下最合適的一個而已
public void say(Object arg){
System.out.println("Object");
}
public void say(int arg){
System.out.println("int");
}
public void say(long arg){
System.out.println("long");
}
public void say(char arg){
System.out.println("char");
}
public void say(Character arg){
System.out.println("Character");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
我先這樣執行:
public static void main(String[] args) {
Test1 test1 = new Test1();
test1.say('a');
}
很容易會想到輸出char,因爲我給的就是char類型
如果我把char類型的方法去掉
public void say(Object arg){
System.out.println("Object");
}
public void say(int arg){
System.out.println("int");
}
public void say(long arg){
System.out.println("long");
}
public void say(Character arg){
System.out.println("Character");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
還是一樣的main方法,參數還是’a’
char類型的’a’自動類型轉換爲int,所以輸出int
再把int對應的方法註釋:
public void say(Object arg){
System.out.println("Object");
}
public void say(long arg){
System.out.println("long");
}
public void say(Character arg){
System.out.println("Character");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
繼續上邊的實驗
int繼續向上轉型爲long
對於基本類型:
-
char -> int -> long -> float -> double
-
char不能轉爲short或byte
繼續上邊實驗
public void say(Object arg){
System.out.println("Object");
}
public void say(Character arg){
System.out.println("Character");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
這次char發生了一次自動裝箱,char -> Character
繼續註釋Character
public void say(Object arg){
System.out.println("Object");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
可以看到char自動裝箱後找不到包裝類型Character,就去找其包裝類型實現的接口類型
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-AXFgaSiX-1574957323112)(C:\Users\12642\AppData\Roaming\Typora\typora-user-images\image-20191128220737974.png)]
最後剩下兩個方法
public void say(Object arg){
System.out.println("Object");
}
public void say(char ... arg){
System.out.println("char ...");
}
包裝類型找不到,包裝類實現的接口類型找不到,就找包裝類的父類啦
二,方法重寫
方法重載時編譯期就根據靜態類型確定了要調用的方法版本,但是方法重寫時在運行期才確定
public class Test2 {
static class Fruit{
public void test(){
System.out.println("Fruit");
}
}
static class Apple extends Fruit{
@Override
public void test(){
System.out.println("Apple");
}
}
static class Banana extends Fruit{
@Override
public void test(){
System.out.println("Banana");
}
}
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit banana = new Banana();
apple.test();//apple
banana.test();//banana
}
}
對於上面的代碼jvm字節碼指令是這樣的
重點看這一段:
public static void main(java.lang.String[]);
Code:
# new指令在堆上開闢空間來給apple分配內存
0: new #2 // class Test2$Apple
3: dup
# 調用apple的構造方法
4: invokespecial #3 // Method Test2$Apple."<init>":()V
# 將構造出來的對象引用存放在main方法局部變量表上
7: astore_1
8: new #4 // class Test2$Banana
11: dup
12: invokespecial #5 // Method Test2$Banana."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method Test2$Fruit.test:()V
20: aload_2
21: invokevirtual #6 // Method Test2$Fruit.test:()V
24: return
完成對象構造
Fruit apple = new Apple();
# new指令在堆上開闢空間來給apple分配內存
0: new #2 // class Test2$Apple
# 將apple引用複製一份放到操作數棧棧頂
3: dup
# 調用apple的構造方法
4: invokespecial #3 // Method Test2$Apple."<init>":()V
# 將構造出來的對象引用存放在main方法局部變量表上
7: astore_1
看方法的調用
apple.test();//apple
字節碼指令:
16: aload_1
17: invokevirtual #6 // Method Test2$Fruit.test:()V
從字節碼註釋中可以看到執行的是Fruit的test方法,但是實際上是執行Apple的test方法纔對,所以這是爲什麼呢?
且兩個不同調用者的invokevirtual參數一樣
17: invokevirtual #6 // Method Test2$Fruit.test:()V
20: aload_2
21: invokevirtual #6 // Method Test2$Fruit.test:()V
我們需要分析invokevirtual指令的執行步驟:
-
找到操作數棧棧頂第一個元素,剛剛也知道操作數棧頂是剛剛new出來的apple的引用,該元素記爲C,其實就是確定實際類型的過程
-
如果在C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權限的校驗,如果通過則返回該方法的直接引用,查找過程結束,否則按照繼承關係從下往上一次查找和驗證
就比如:
17: invokevirtual #6 // Method Test2$Fruit.test:()V
去常量池找索引爲6的描述符就是這個 Method Test2$Fruit.test:()V
可以看到他們相同的符號引用,但是卻被解析到了不同的直接引用上,這是用爲,invokevirtual第一步是在運行期確定方法調用者的實際類型,這也正是方法重寫的本質,運行期根據實際類型確定方法執行版本的分派也叫作動態分派
針對i這種動態分派的過程,虛擬機會在方法區建立一個叫做虛方法表的數據結構
虛方法表中存放着各個方法的實際地址,如果某個方法在子類中沒有重寫,那麼子類和父類的方法的實際地址就是一樣的,都指向父類方法的實際地址,如果子類重寫了父類的某個方法,那麼子類虛方法表中該方法的實際地址就是子類實現版本的地址,其實虛方法表可以看成是一個哈希表,因爲動態分派的的方法版本選擇過程是需要運行時在類的方法元數據中搜索合適的目標方法,現在改用虛方法表可以代替元數據查詢,直接找方法表得到方法實際地址,提高了性能