java中的靜態繫結與動態繫結

我爱吃炸鸡發表於2024-11-13

個人學習記錄,歡迎大家指導

什麼是多型?

一個引用變數,它可以引用任何子類的物件,這個引用變數是多型的。

繫結

將一個方法呼叫與對應方法主體關聯起來被稱為繫結。(也就是,執行一條方法呼叫語句時所對應執行的方法體,叫做該方法體和這條方法呼叫語句繫結了)

動態繫結

來看一段程式碼

public class Parent {

    public void m(){
        System.out.println("父類方法執行");
    }

    public static void main(String[] arg){
        List<Parent> list = Arrays.asList(new A(),new B(),new C());
        for(Parent p : list){
            p.m();
        }
    }
}

class A extends Parent{
    public void m(){
        System.out.println("A類方法執行");
    }
}

class B extends Parent{
    public void m(){
        System.out.println("B類方法執行");
    }
}

class C extends Parent{
    public void m(){
        System.out.println("C類方法執行");
    }
}

output:
A類方法執行
B類方法執行
C類方法執行

何為動態繫結?
上述程式碼中,第一次迴圈,p變數接受一個A物件,p.m()語句呼叫A的m()方法,第二次迴圈時,p變數接受一個B物件,p.m()語句呼叫B的m()方法,第三次迴圈時,p變數接受一個C物件,p.m()語句呼叫C的m()方法,3次迴圈都是執行的p.m()語句,每次迴圈p.m()語句繫結的方法體不一樣,這種執行時根據變數p實際引用物件動態繫結方法體的方式,叫做動態繫結。動態繫結是為了執行子類重寫的方法。

靜態繫結

對於static、final、private修飾的方法,以及構造方法,這些方法不可被子類重寫,它們採用靜態繫結。

方法呼叫的具體過程

看一段程式碼

public class Fu {

    public void m(Object obj){
        System.out.println("父類Object方法執行");
    }

    public static void main(String[] args){
        Fu f = new Zi();
        f.m(1);
    }

}

class Zi extends Fu{

    public void m(Object obj){
        System.out.println("子類Object方法執行");
    }

    public void m(Integer i){
        System.out.println("子類Integer方法執行");
    }
}

output:
子類Object方法執行

為啥選擇子類的m(Object)方法執行,而不是m(Integer)方法執行呢,你肯定想說因為向上轉型,不能執行子類特有的方法,確實是這樣,但為什麼呢?

  • 來看一下方法呼叫的過程
    編譯期:
    1、編譯器根據變數f宣告的型別Fu,去Fu類中查詢名字為m的方法。
    2、此時可能找到多個名為m的方法,因為方法是可以過載的,編譯器再根據呼叫方法時提供的引數型別,去找到最匹配的方法,在這裡是m(Object)方法,
    這個過程叫做過載解析。過載解析後,編譯器獲得了方法的簽名(方法名+形參型別)。
    3、如果方法是被static、final、private修飾的,該方法不能被重寫,f.m(1)執行時,無論f引用什麼子類物件,都是執行的父類的方法,此時可以確定執行時無論f的物件是什麼都是執行父類中的方法,繫結Fu類中的m(Object)方法。此為前期繫結,也叫靜態繫結,是編譯期時發生的繫結。
    4、如果方法不被static、final、private修飾,編譯器採用動態繫結,記錄方法簽名m(Object)。
    執行期:
    執行f.m(1)位元組碼時
    1、如果是靜態繫結,直接執行繫結的方法,不關心物件型別。
    2、如果是動態繫結,編譯期只確定了方法簽名,方法所在類未確定,根據f引用物件的實際型別,去該型別中找到簽名為m(Object)的方法,執行該方法,此時執行時才把方法呼叫與具體方法體關聯起來。

動態繫結就是執行的時候才決定執行哪個方法體,靜態繫結在編譯期就確定了要執行的方法體。
動態繫結在編譯期確定執行的方法簽名,在執行期確定執行哪個類的方法,實現自由執行子類重寫的方法。靜態繫結在編譯期決定執行哪個類中的哪個方法,因為這些方法不能被重寫,子類物件呼叫也是呼叫的父類中的,可以明確方法體,對於這類不能重寫的方法,採用靜態繫結效能更好,雖然採用動態繫結也能實現。

一個靜態繫結的例子

public class Fu {

    private void m(){
        System.out.println("父類private方法執行");
    }

    public static void main(String[] args){
        Fu f = new Zi();
        f.m();
    }
}

public class Zi extends Fu{

    public void m(){
        System.out.println("子類public方法執行");
    }

    public static void main(String[] args){
        Fu f = new Zi();
       // f.m(); 無法編譯
    }
}

output:
父類private方法執行

補充一個繼承泛型類的例子

public class GenericClazz<T> {

    public void m(T t){
        System.out.println("泛型類中m()方法執行");
    }
}

public class Zi extends GenericClazz<Integer>{

    public void m(Integer i){
        System.out.println("子類中m()方法執行");
    }

//    public void m(Object i){
//        System.out.println("子類中m()方法執行");
//    } 無法編譯

    public static void main(String[] args){
        GenericClazz f = new Zi();
        f.m(1);
    }
}

output:
子類中m()方法執行

泛型類中,由於有泛型擦除,T實際會被在編譯時替換為Object型別,那麼Zi類繼承GenericClazz類就是繼承的m(Object)方法,而m(Integer)是對m(Object)的過載而不是重寫,在用f.m(1)呼叫時,按照上面的理論,執行時應該是呼叫的Zi類中的m(Object)方法,而m(Object)子類沒有重寫,應該走父類語句,但是這裡卻走了子類的m(Integer)方法,原因是編譯器自動在子類做了m(Object)方法的重寫,會執行類似下面的語句:

public void m(Object t){
    m((Integer) t);
}

讓m(Integer)看起來就像是重寫的父類的m(Integer)一樣,並且還關閉了子類對m(Object)的手動重寫,可以看到上面手動重寫m(Object)編譯是報錯的。

相關文章