理解Java程式的執行

真正的飛魚發表於2023-04-22

main 方法

public class Solution {
    public static void main(String[] args) {
        Person person = new Person();
        person.hello();
    }
}

class Person {
    public void hello() {
        System.out.println("hello");
    }
}

原始檔名是 Solution.java,這是因為檔名必須與 public 類的名字相匹配。在一個原始檔中,只能有一個公有類,但可以有任意數目的非公有類。

在這個示例程式中包含兩個類:Person 類和帶有 public 訪問修飾符的 Solution 類。Solution 類包含了 main 方法。

當編譯這段原始碼的時候,編譯器將在目錄下建立兩個 class 檔案:Solution.class 和 Person.class。

將程式中包含 main 方法的類名提供給位元組碼直譯器,以便啟動這個程式:java Solution。位元組碼直譯器開始執行 Solution 類的 main 方法中的程式碼。

image-20230326211555782.png

理解方法呼叫

下面假設要呼叫 x.f(args)。下面是呼叫過程的詳細描述:

  • 編譯時:
    1. 編譯器査看物件變數的宣告型別和方法名,然後獲得所有可能被呼叫的候選方法。
    2. 編譯器査看呼叫方法時提供的引數型別,然後獲得需要呼叫的方法名字和引數型別。
  • 執行時:
    • 如果方法是 private 方法、static 方法、final 方法或者構造器方法,那麼編譯器將可以準確地知道應該呼叫哪個方法,我們將這種呼叫方式稱為靜態繫結(static binding)。與此對應的是,呼叫的方法依賴於物件變數 x 的實際型別,並且在執行時實現動態繫結。
    • 虛擬機器預先為每個類建立了一個方法表(method table),方法表中列出了所有方法的簽名和實際呼叫的方法。這樣一來,在真正呼叫方法的時候,虛擬機器僅查詢方法表就行了。

1、編譯器査看物件變數的宣告型別和方法名,然後獲得所有可能被呼叫的候選方法。假設呼叫 x.f(param),且物件變數 x 被宣告為 C 型別。需要注意的是:有可能存在多個名字為 f,但引數型別不一樣的方法。例如,可能存在方法 f(int) 和方法 f(String)。編譯器將會一一列舉所有 C 類中名為 f 的方法和其父類中訪問屬性為 public 且名為 f 的方法(父類的私有方法不可訪問)。至此,編譯器已獲得所有可能被呼叫的候選方法。

2、接下來,編譯器將査看呼叫方法時提供的引數型別,然後獲得需要呼叫的方法名字和引數型別。如果在所有名為 f 的方法中存在一個與提供的引數型別完全匹配,就選擇這個方法。這個過程被稱為過載解析(overloading resolution)。例如,對於呼叫 x.f("Hello") 來說,編譯器將會挑選 f(String),而不是 f(int)。由於允許型別轉換(int 可以轉換成 double,Manager 可以轉換成 Employee 等),所以這個過程可能很複雜。如果編譯器沒有找到與引數型別匹配的方法,或者發現經過型別轉換後有多個方法與之匹配,就會報告一個錯誤。至此,編譯器已獲得需要呼叫的方法名字和引數型別。

如果方法是 private 方法、static 方法、final 方法或者構造器方法,那麼編譯器將可以準確地知道應該呼叫哪個方法,我們將這種呼叫方式稱為靜態繫結(static binding)。與此對應的是,呼叫的方法依賴於物件變數 x 的實際型別,並且在執行時實現動態繫結。在我們列舉的示例中,編譯器採用動態繫結的方式生成一條呼叫 f(String) 的指令。

當程式執行,並且採用動態繫結呼叫方法時,虛擬機器一定呼叫與物件變數 x 所引用的物件的實際型別最合適的那個類的方法。假設 x 的實際型別是 D,D 是 C 類的子類。如果 D 類定義了 f(String) 方法,虛擬機器就直接呼叫 D 類的 f(String) 方法;否則(D 類中沒有定義 f(String) 方法),將在 D 類的父類中尋找 f(String),以此類推。


每次呼叫方法都要進行搜尋,時間開銷相當大。因此,虛擬機器預先為每個類建立了一個方法表(method table),方法表中列出了所有方法的簽名(方法名、引數型別)和實際呼叫的方法。這樣一來,在真正呼叫方法的時候,虛擬機器僅查詢方法表就行了。

在前面的例子中,虛擬機器搜尋 D 類的方法表,以便尋找與呼叫 f(Sting) 相匹配的方法。這個方法既有可能是 D.f(String),也有可能是 C.f(String),這裡的 C 是 D 的父類。這裡需要提醒一點,如果呼叫 super.f(param),編譯器將對物件變數父類的方法表進行搜尋。

方法呼叫的示例

public static void main(String[] args) {
    Employee e = new Manager("Carl Cracker", 80000, 1987, 12, 15);
    System.out.println("salary=" + e.getSalary());
}

現在,檢視一下呼叫 e.getSalary() 的詳細過程。物件變數 e 被宣告為 Employee 型別。Employee 類只有一個名叫 getSalary() 的方法,這個方法沒有引數。因此,在這裡不必擔心過載解析的問題。

由於 getSalary() 不是 private 方法、static 方法,也不是 final 方法,所以將採用動態繫結。虛擬機器為 Employee 和 Manager 兩個類生成方法表。


在 Employee 的方法表中,列出了這個類定義的方法。實際上,下面列出的方法並不完整,Employee 類有一個父類 Object,Employee 類從這個父類中還繼承了許多方法,在此我們略去了 Object 的方法。

Employee:
    getName() -> Employee.getName()
    getSalary() -> Employee.getSalary()
    getHireDay() -> Employee.getHireDay()
    raiseSalary(double) -> Employee.raiseSalary(doubl e)

Manager 的方法表稍微有些不同。其中有三個方法是繼承而來的,一個方法是重新定義的(方法的重寫),還有一個方法是新增加的。

Manager:
    getName() -> Employee.getName()
    getSalary() -> Manager.getSalary()
    getHireDay() -> Employee.getHireDay()
    raiseSalary(double) -> Employee.raiseSalary(doubl e)
    setBonus(double) -> Manager.setBonus(double)

在執行時,呼叫 e.getSalary() 的解析過程為:

  1. 首先,虛擬機器提取物件變數 e 的實際型別的方法表。既可能是 Employee、Manager 的方法表,也可能是 Employee 類的其他子類的方法表。
  2. 接下來,虛擬機器搜尋物件變數 e 的實際型別的方法表。此時,虛擬機器已經知道應該呼叫哪個方法。
  3. 最後,虛擬機器呼叫方法。

動態繫結有一個非常重要的特性:無需對現存的程式碼進行修改,就可以對程式進行擴充套件。假設增加一個新類 Executive,並且物件變數 e 有可能引用這個類的物件,我們不需要對包含呼叫 e.getSalary() 的程式碼進行重新編譯。如果 e 恰好引用一個 Executive 類的物件,就會自動地呼叫 Executive.getSalary() 方法。

參考資料

《Java核心技術卷一:基礎知識》(第10版)第 5 章:繼承 5.1.6 理解方法呼叫

相關文章