1. 程式人生 > >Java靜態繫結與動態繫結

Java靜態繫結與動態繫結

程式繫結的概念:
繫結指的是一個方法的呼叫與方法所在的類(方法主體)關聯起來。對java來說,繫結分為靜態繫結和動態繫結;或者叫做前期繫結和後期繫結.

靜態繫結:
在程式執行前方法已經被繫結(也就是說在編譯過程中就已經知道這個方法到底是哪個類中的方法),此時由編譯器或其它連線程式實現。例如:C。
針對java簡單的可以理解為程式編譯期的繫結;這裡特別說明一點,java當中的方法只有final,static,private和構造方法是前期繫結

動態繫結:
後期繫結:在執行時根據具體物件的型別進行繫結
若一種語言實現了後期繫結,同時必須提供一些機制,可在執行期間判斷物件的型別,並分別呼叫適當的方法。也就是說,編譯器此時依然不知道物件的型別,但方法呼叫機制能自己去調查
,找到正確的方法主體。不同的語言對後期繫結的實現方法是有所區別的。但我們至少可以這樣認為:它們都要在物件中安插某些特殊型別的資訊。
動態繫結的過程:
  1. 虛擬機器提取物件的實際型別的方法表;
  2. 虛擬機器搜尋方法簽名;
  3. 呼叫方法。

關於final,static,private和構造方法是前期繫結的理解
對於private的方法,首先一點它不能被繼承,既然不能被繼承那麼就沒辦法通過它子類的物件來呼叫,而只能通過這個類自身的物件來呼叫。因此就可以說private方法和定義這個方法的類繫結在了一起。
final方法雖然可以被繼承,但不能被重寫(覆蓋),雖然子類物件可以呼叫,但是呼叫的都是父類中所定義的那個final方法,(由此我們可以知道將方法宣告為final型別,一是為了防止方法被覆蓋,二是為了有效地關閉java中的動態繫結)。
構造方法也是不能被繼承的(網上也有說子類無條件地繼承父類的無引數建構函式作為自己的建構函式,不過個人認為這個說法不太恰當,因為我們知道子類是通過super()來呼叫父類的無參構造方法,來完成對父類的初始化, 而我們使用從父類繼承過來的方法是不用這樣做的,因此不應該說子類繼承了父類的構造方法),因此編譯時也可以知道這個構造方法到底是屬於哪個類。
對於static方法,具體的原理我也說不太清。不過根據網上的資料和我自己做的實驗可以得出結論:static方法可以被子類繼承,但是不能被子類重寫(覆蓋),但是可以被子類隱藏。(這裡意思是說如果父類裡有一個static方法,它的子類裡如果沒有對應的方法,那麼當子類物件呼叫這個方法時就會使用父類中的方法。而如果子類中定義了相同的方法,則會呼叫子類的中定義的方法。唯一的不同就是,當子類物件上轉型為父類物件時,不論子類中有沒有定義這個靜態方法,該物件都會使用父類中的靜態方法。因此這裡說靜態方法可以被隱藏而不能被覆蓋。這與子類隱藏父類中的成員變數是一樣的。隱藏和覆蓋的區別在於,子類物件轉換成父類物件後,能夠訪問父類被隱藏的變數和方法,而不能訪問父類被覆蓋的方法)

由上面我們可以得出結論,如果一個方法不可被繼承或者繼承後不可被覆蓋,那麼這個方法就採用的靜態繫結。

java的編譯與執行
java的編譯過程是將java原始檔編譯成位元組碼(jvm可執行程式碼,即.class檔案)的過程,在這個過程中java是不與記憶體打交道的,在這個過程中編譯器會進行語法的分析,如果語法不正確就會報錯。
Java的執行過程是指jvm(java虛擬機器)裝載位元組碼檔案並解釋執行。在這個過程才是真正的創立記憶體佈局,執行java程式。
java位元組碼的執行有兩種方式: (1)即時編譯方式:直譯器先將位元組編譯成機器碼,然後再執行該機器碼;(2)解釋執行方式:直譯器通過每次解釋並執行一小段程式碼來完成java位元組碼程式的所有操作。(這裡我們可以看出java程式在執行過程中其實是進行了兩次轉換,先轉成位元組碼再轉換成機器碼。這也正是java能一次編譯,到處執行的原因。在不同的平臺上裝上對應的java虛擬機器,就可以實現相同的位元組碼轉換成不同平臺上的機器碼,從而在不同的平臺上執行)

前面已經說了對於java當中的方法而言,除了final,static,private
和構造方法是前期繫結外,其他的方法全部為動態繫結。
而動態繫結的典型發生在父類和子類的轉換宣告之下:
比如:Parent p = new Children();
其具體過程細節如下:
1:編譯器檢查物件的宣告型別和方法名。
假設我們呼叫x.f(args)方法,並且x已經被宣告為C類的物件,那麼編譯器會列舉出C 類中所有的名稱為f 的方法和從C 類的超類繼承過來的f 方法。
2:接下來編譯器檢查方法呼叫中提供的引數型別。
如果在所有名稱為f 的方法中有一個引數型別和呼叫提供的引數型別最為匹配,那麼就呼叫這個方法,這個過程叫做“過載解析”。

3:當程式執行並且使用動態繫結呼叫方法時,虛擬機器必須呼叫同x所指向的物件的實際型別相匹配的方法版本。

假設實際型別為D(C的子類),如果D類定義了f(String)那麼該方法被呼叫,否則就在D的超類中搜尋方法f(String),依次類推。

JAVA 虛擬機器呼叫一個類方法時(靜態方法),它會基於物件引用的型別(通常在編譯時可知)來選擇所呼叫的方法。相反,當虛擬機器呼叫一個例項方法時,它會基於物件實際的型別(只能在執行時得知)來選擇所呼叫的方法,這就是動態繫結,是多型的一種。動態繫結為解決實際的業務問題提供了很大的靈活性,是一種非常優美的機制。

與方法不同,在處理java類中的成員變數(例項變數和類變數)時,並不是採用執行時繫結,而是一般意義上的靜態繫結。所以在向上轉型的情況下,物件的方法可以找到子類,而物件的屬性(成員變數)還是父類的屬性(子類對父類成員變數的隱藏)。
Java程式碼
public class Father {
    protected String name = "父親屬性";
}
  

public class Son extends Father {
    protected String name = "兒子屬性";

    public static void main(String[] args) {
        Father sample = new Son();
        System.out.println("呼叫的屬性:" + sample.name);
    }
}

結論,呼叫的成員為父親的屬性。
這個結果表明,子類的物件(由父類的引用handle)呼叫到的是父類的成員變數。所以必須明確,執行時(動態)繫結針對的範疇只是物件的方法
現在試圖呼叫子類的成員變數name,該怎麼做?最簡單的辦法是將該成員變數封裝成方法getter形式
程式碼如下:
Java程式碼
public class Father {
    protected String name = "父親屬性";

    public String getName() {
        return name;
    }
}  

public class Son extends Father {
    protected String name = "兒子屬性";

    public String getName() {
        return name;
    }

    public static void main(String[] args) {
        Father sample = new Son();
        System.out.println("呼叫的屬性:" + sample.getName());
    }
}

結果:呼叫的是兒子的屬性
java因為什麼對屬性要採取靜態的繫結方法。這是因為靜態繫結是有很多的好處,它可以讓我們在編譯期就發現程式中的錯誤,而不是在執行期。這樣就可以提高程式的執行效率!而對方法採取動態繫結是為了實現多型,多型是java的一大特色。多型也是面向物件的關鍵技術之一,所以java是以效率為代價來實現多型這是很值得的。

注:以上內容大部分來自網際網路,小部分是個人見解,絕非權威性言論。如有語言表達不當或者表述不正確的地方,萬望指教。