在程式執行時,進行方法呼叫是最普遍,最頻繁的操作
方法呼叫不等於方法執行:
#方法呼叫階段唯一的任務就是確定被呼叫的方法版本,即呼叫哪一個方法
不涉及方法內部的具體運行過程
Class檔案的編譯過程不包含傳統編譯中的連線步驟
Class檔案中的一切方法呼叫在Class檔案裡面儲存的都是符號參考,而不是方法在實際運行時記憶體佈局中的入口位址,即之前的直接引用:
#這樣使得Java具有更強大的動態擴展能力
#同時也使得Java方法呼叫過程變得相對複雜
需要在類別載入期間,甚至會到運行期間才能確定目標方法的直接引用
所有方法呼叫中的目標方法在Class檔案裡都是常數池的參考
在類別的載入解析階段,會將其中的一部分符號引用轉換為直接引用:
方法在程式真正執行之前就有一個可確定的呼叫版本,並且這個方法的呼叫版本在運行期是不可改變的
也就是說,呼叫目標在程式碼完成,編譯器進行編譯時就必須確定下來,這也叫做方法解析
在Java中符合"編譯期可知,運行期不可變" 的方法有兩大類別:
#靜態方法: 與型別直接關聯
私有方法: 在外部不可被存取
這兩種方法各自的特點決定這兩種方法都不可能透過繼承或別的方式來重寫版本,因此適合在類別載入階段進行解析
非虛方法: 在類別載入階段會把符號引用解析為該方法的直接引用
靜態方法
私人方法
#實例建構子
父類別方法
虛擬方法: 在類別載入階段不會將符號參考解析為該方法的直接引用
除去以上的非虛方法,其它的方法皆為虛擬方法
public class StaticDispatch { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public static void sayHello(Human guy) { System.out.println("Hello, Guy!"); } public static void sayHello(Man guy) { System.out.println("Hello, Gentleman!"); } public static void sayHello(woman guy) { System.out.println("Hello, Lady!"); } public static void main(String[] args) { Human man = new Man(); Human women = new Woman(); sayHello(man); sayHello(woman); } }
Human man = new Human();
## Human
為變數的靜態型別Man
為變數的實際型別靜態型別和實際型別在程式中都會放生變化:
靜態類型:
靜態類型的變化僅在使用時發生
實際類型變化的結果在運行期才確定下來 #編譯器在編譯期間並不知道一個物件的實際類型是什麼
Human human = new Man(); sayHello(man); sayHello((Man)man); // 类型转换,静态类型变化,转型后的静态类型一定是Man man = new woman(); // 实际类型变化,实际类型是不确定的 sayHello(man); sayHello((Woman)man); // 类型转换,静态类型变化
#靜態分派:#所有依賴靜態型別來定位方法的執行版本的分派動作
典型應用程式#由於字面量沒有顯示靜態類型,只能通過語言上的規則去理解和推斷
public class LiteralTest { public static void sayHello(char arg) { System.out.println("Hello, char!"); } public static void sayHello(int arg) { System.out.println("Hello, int!"); } public static void sayHello(long arg) { System.out.println("Hello, long!"); } public static void sayHello(Character arg) { System.out.println("Hello, Character!"); } public static void main(String[] arg) { sayHello('a'); } }編譯器將重載方法從上向下依序註解,得到不同的輸出
#如果編譯器無法確定要自定轉型為哪種類型,會提示類型模糊,拒絕編譯
public class LiteralTest { public static void sayHello(String arg) { // 新增重载方法 System.out.println("Hello, String!"); } public static void sayHello(char arg) { System.out.println("Hello, char!"); } public static void sayHello(int arg) { System.out.println("Hello, int!"); } public static void sayHello(long arg) { System.out.println("Hello, long!"); } public static void sayHello(Character arg) { System.out.println("Hello, Character!"); } public static void main(String[] args) { Random r = new Random(); String s = "abc"; int i = 0; sayHello(r.nextInt() % 2 != 0 ? s : 1 ); // 编译错误 sayHello(r.nextInt() % 2 != 0 ? 'a' : false); //编译错误 } }動態分派
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 Woman extends Human { @override protected void sayHello() { System.out.println("Woman Say Hello!"); } } public static void main(String[] args) { Human man = new Man(); Human women = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } }這裡不是根據靜態型別決定的
Human兩個變數man
和###woman###在呼叫###sayHello()### 方法時執行了不同的行為###########變數###man# ##在兩個呼叫中執行了不同的方法############導致這個現象的額原因###:這兩個變數的實際型別不同######## ####Java虛擬機器是如何根據實際類型分派方法的執行版本的:### 從###invokevirtual###指令的多態查找過程開始###,invokevirtual###指令執行時間解析過程大致分為以下幾個步驟:############找到操作數棧頂的第一個元素所指向的物件的實際型別,記作###C######如果在類型C中找到與常數中的描述符和簡單名稱相符合的方法,然後進行訪問權限驗證,如果驗證通過則返回這個方法的直接引用,查找過程結束;如果驗證不通過,則拋出java.lang.illegalAccessError異常
#如果未找到,就依照繼承關係從下往上依序對類型C的各個父類別進行第二步驟的搜尋和驗證過程
如果總是沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常
invokevirtual指令執行的第一步就是在運行時期確定接收者的實際類型,所以兩次呼叫中的invokevirtual指令把常數池中的類別方法符號引用解析到了不同的直接引用上
這種在運行時期根據實際類型確定方法執行版本的分派過程就叫做動態分派
虛擬機概念解析的模式就是靜態分派和動態分派,可以理解虛擬機在分派中"會做什麼" 這個問題
虛擬機器"具體是如何做到的" 在各種虛擬機器實作上會有差別:
由於動態分派是非常頻繁的動作,而且動態分派的方法版本選擇過程需要運行時在類別的方法元資料中搜尋合適的目標方法
因此在虛擬機的實際實現中,為了基於性能的考慮,大部分實現都不會真正的進行如此頻繁的搜索
最常用的"穩定優化"的方式是為類別在方法區中建立一個虛擬方法表(Virtual Method Table,即vtable), 使用虛擬方法表索引代替元資料查找以提高效能
虛擬方法表中存放著各個方法的實際入口位址:
#如果某個方法在子類別中沒有被重寫,那子類別的虛擬方法表裡面的位址入口和父類別相同方法的位址入口是一致的,都指向父類別的實際入口
如果子類別中重寫了這個方法,子類別方法表中的位址將會替換為指向子類別實際方法的入口位址
#具有相同簽章的方法,在父類別,子類別的虛擬方法表中具有相同的索引序號:
這樣當類型變換時,僅僅需要變更查找的方法表,就可以從不同的虛擬方法表中按索引轉換出所需的入口位址
方法表一般在類別載入階段的連接階段進行初始化:
準備了類別的變數初始值後,虛擬機會把該類別的方法表也初始化完畢
以上是使用Java方法呼叫來解析靜態分派和動態分派的詳細內容。更多資訊請關注PHP中文網其他相關文章!