1. 程式人生 > >Java方法的靜態繫結與動態繫結講解(向上轉型的執行機制詳解)

Java方法的靜態繫結與動態繫結講解(向上轉型的執行機制詳解)

   今天看設計模式-模板方法模式時發現一個實現父類呼叫子類方法的效果的程式碼,不理解其中的原理,然後詢問大佬之後,發現這原來是動態繫結的知識,所以惡補了一下。

package com.practice;

/**
 * 父類呼叫子類的方法
 *
 * @author ling
 * @since 2018年09月11日
 */
public class SuperClassInvokeMethodOfSubClass {
    public static void main(String[] args) {
        SubClass subClass=new SubClass();
        subClass.setOk(false);
        subClass.say();
        System.out.println("");

        SuperClass superClass=new SubClass();
        superClass.say();
    }
}
class SuperClass{
    protected boolean isOk(){
        return true;
    }
    public void say(){
        if(isOk()){
            System.out.println("it true");
        }else{
            System.out.println("it false");
        }
    }
}
class SubClass extends SuperClass {
    private boolean isOk=false;
    public void setOk(boolean isOk){
        this.isOk=isOk;
    }
    @Override
    protected boolean isOk(){
        System.out.println("subclass-----------------------");
        return isOk;
    }
}
輸出結果
subclass-----------------------
it false

subclass-----------------------
it false

正常情況,subClass.say()中呼叫的isOk()應該是父類的方法,但為啥輸出是false呢???

動態繫結看的是呼叫方法的物件的實際型別,因為呼叫say方法隱式傳遞了this物件,這個say方法中的this是子類物件,當然會呼叫子類的isOK()。所以父類呼叫子類方法的效果

動態繫結

在面向物件的程式設計語言中,多型是繼資料抽象和繼承之後的第三種基本特性。多型通過分離做什麼和怎麼做,從另一個角度將介面和實現分離開來。在一開始接觸多型這個詞的時候,我們或許會因為這個詞本身而感到困惑,如果我們把多型改稱作“動態繫結”,相信很多人就能理解他的深層含義。通常的,我們把動態繫結也叫做後期繫結,執行時繫結。

(一)方法呼叫繫結

1.繫結概念

通常,把一個方法與其所在的類/物件 關聯起來叫做方法的繫結。如果在程式執行前進行繫結,我們將這種繫結方法稱作前期繫結。在面向過程語言中,比如c,這種方法是預設的也是唯一的。如果我們在java中採用前期繫結,很有可能編譯器會因為在這龐大的繼承實現體系中去繫結哪個方法而感到迷惑。解決的辦法就是動態繫結,這種後期繫結的方法,在執行的時候根據物件的型別進行繫結。

在java中,動態繫結是預設的行為。但是在類中,普通的方法會採用這種動態繫結的方法,也有一些情況並不會自然的發生動態繫結。

  1)靜態繫結

        靜態繫結(前期繫結)是指:在程式執行前就已經知道方法是屬於那個類的,在編譯的時候就可以連線到類的中,定位到這個方法。

        在Java中,final、private、static修飾的方法以及建構函式都是靜態繫結的,不需程式執行,不需具體的例項物件就可以知道這個方法的具體內容。

    2)動態繫結

        動態繫結(後期繫結)是指:在程式執行過程中,根據具體的例項物件才能具體確定是哪個方法。

        動態繫結是多型性得以實現的重要因素,它通過方法表來實現:每個類被載入到虛擬機器時,在方法區儲存元資料,其中,包括一個叫做 方法表(method table)的東西,表中記錄了這個類定義的方法的指標,每個表項指向一個具體的方法程式碼。如果這個類重寫了父類中的某個方法,則對應表項指向新的程式碼實現處。從父類繼承來的方法位於子類定義的方法的前面。

        動態繫結語句的編譯、執行原理:我們假設 Father ft=new Son();  ft.say();  Son繼承自Father,重寫了say()。

        1:編譯:我們知道,向上轉型時,用父類引用執行子類物件,並可以用父類引用呼叫子類中重寫了的同名方法。但是不能呼叫子類中新增的方法,為什麼呢?

          因為在程式碼的編譯階段,編譯器通過 宣告物件的型別(即引用本身的型別) 在方法區中該型別的方法表中查詢匹配的方法(最佳匹配法:引數型別最接近的被呼叫),如果有則編譯通過。(這裡是根據宣告的物件型別來查詢的,所以此處是查詢 Father類的方法表,而Father類方法表中是沒有子類新增的方法的,所以不能呼叫。)

         編譯階段是確保方法的存在性,保證程式能順利、安全執行。

        2:執行:我們又知道,ft.say()呼叫的是Son中的say(),這不就與上面說的,查詢Father類的方法表的匹配方法矛盾了嗎?不,這裡就是動態繫結機制的真正體現。

         上面編譯階段在 宣告物件型別 的方法表中查詢方法,只是為了安全地通過編譯(也為了檢驗方法是否是存在的)。而在實際執行這條語句時,在執行 Father ft=new Son(); 這一句時建立了一個Son例項物件,然後在 ft.say() 呼叫方法時,JVM會把剛才的son物件壓入運算元棧,用它來進行呼叫。而用例項物件進行方法呼叫的過程就是動態繫結:根據例項物件所屬的型別去查詢它的方法表,找到匹配的方法進行呼叫。我們知道,子類中如果重寫了父類的方法,則方法表中同名表項會指向子類的方法程式碼;若無重寫,則按照父類中的方法表順序儲存在子類方法表中。故此:動態繫結根據物件的型別的方法表查詢方法是一定會匹配(因為編譯時在父類方法表中以及查詢並匹配成功了,說明方法是存在的。這也解釋了為何向上轉型時父類引用不能呼叫子類新增的方法:在父類方法表中必須先對這個方法的存在性進行檢驗,如果在執行時才檢驗就容易出危險——可能子類中也沒有這個方法)。

      3:區分

        程式在JVM執行過程中,會把類的型別資訊、static屬性和方法、final常量等元資料載入到方法區,這些在類被載入時就已經知道,不需物件的建立就能訪問的,就是靜態繫結的內容;需要等物件創建出來,使用時根據堆中的例項物件的型別才進行取用的就是動態繫結的內容。

2.final修飾

如果一個屬性被final修飾,則含義是:在初始化之後不能被更改。 
如果一個方法被final修飾,含義則是不能被覆蓋。我們常常喜歡從巨集觀的角度這樣說,但是我們真正的被final修飾的方法為什麼不能被覆蓋呢?因為final修飾詞其實實際上關閉了動態繫結。在java中被final修飾的內容不能採用動態繫結的方法,不能動態繫結就沒有多型的概念,自然也就不能被覆蓋。

3.“覆蓋”私有方法

其實我們很少把方法設定為私有。如果我們將private方法“覆蓋”掉,其實我們獲得的只是一個新的方法。完全和父類沒關係了。這一點要注意,或許面試的時候會被問到:在子類中“覆蓋”父類私有方法是被允許而不報錯的,只不過完全是兩個沒關係的方法罷了。

4.域與靜態方法

當我們瞭解了多型性之後可能會認為所有的事物都是可以多型地發生。其實並不是,如果我們直接訪問某個域,這個訪問會在編譯期進行解析,我們可以參考下面的例子:

package Polymorphic;

/**
 * 
 * @author QuinnNorris
 * 域不具有多型性
 */
public class polymorphics {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Super sup = new Sub();
        System.out.println("sup.field = " + sup.field + ", sup.getField() = "
                + sup.getField());
        Sub sub = new Sub();
        System.out.println("sub.field = " + sub.field + ", sub.getField() = "
                + sub.getField() + ", sub.getSuperField() = "
                + sub.getSuperField());
    }

}

class Super {
    public int field = 0;

    public int getField() {
        return field;
    }
}

class Sub extends Super {
    public int field = 1;

    public int getField() {
        return field;
    }

    public int getSuperField() {
        return super.field;
    }
}
輸出結果: 
sup.field = 0, sup.getField() = 1 
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0

這個例子告訴我們,當我們呼叫一個方法時,去選擇執行哪個方法的主體是執行時動態選擇的。但是當我們直接訪問例項域的時候,編譯器直接按照這個物件所表示的型別來訪問。於此情況完全相同的還有靜態方法。所以我們可以做出這種總結:

  1. 普通方法:根據物件實體的型別動態繫結
  2. 域和靜態方法:根據物件所表現的型別前期繫結

  通俗地講,普通的方法我們看new後面的是什麼型別;域和靜態方法我們看=前面宣告的是什麼型別。 
儘管這看來好像是一個非常容易讓人混懸哦的問題。但是在實踐中,實際上從來(或者說很少)不會發生。首先,那些不把例項域設定為private的程式設計師基本上已經全都被炒魷魚了(例項域很少被修飾成public)。其次我們很少會將自己在子類中建立的域設定成和父類一樣的名字。