天天看點

Java虛拟機:靜态分派和動态分派重寫和重載靜态分派和重載動态分派與重寫

參考資料《深入了解java虛拟機(第3版)》

目錄

  • 重寫和重載
  • 靜态分派和重載
    • 重載方法比對優先級
  • 動态分派與重寫
    • 字段永遠不會參與多态
    • 靜态分派是多分派,動态分派是單分派

重寫和重載

  • 重寫是子類對父類的允許通路的方法的實作過程進行重新編寫, 傳回值和形參都不能改變。即外殼不變,内容重寫!
    • 參數清單與被重寫方法的參數清單必須完全相同。
    • 傳回類型與被重寫方法的傳回類型可以不相同,但是必須是父類傳回值的派生類(java5 及更早版本傳回類型要一樣,java7 及更高版本可以不同)。
    • 通路權限不能比父類中被重寫的方法的通路權限更低。例如:如果父類的一個方法被聲明為 public,那麼在子類中重寫該方法就不能聲明為 protected。
    • 父類的成員方法隻能被它的子類重寫。
    • 聲明為 final 的方法不能被重寫。
    • 聲明為 static 的方法不能被重寫,但是能夠被再次聲明。
    • 子類和父類在同一個包中,那麼子類可以重寫父類所有方法,除了聲明為 private 和 final 的方法。
    • 子類和父類不在同一個包中,那麼子類隻能夠重寫父類的聲明為 public 和 protected 的非 final 方法。
    • 重寫的方法能夠抛出任何非強制異常,無論被重寫的方法是否抛出異常。但是,重寫的方法不能抛出新的強制性異常,或者比被重寫方法聲明的更廣泛的強制性異常,反之則可以。
    • 構造方法不能被重寫。
    • 如果不能繼承一個類,則不能重寫該類的方法。
  • 重載(overloading) 是在一個類裡面,方法名字相同,而參數不同。傳回類型可以相同也可以不同。
    • 每個重載的方法(或者構造函數)都必須有一個獨一無二的參數類型清單。
    • 僅傳回值不同不能叫重載。

靜态分派和重載

public class StaticDispatch {
    static abstract class Human {

    }

    static class Man extends Human {

    }

    static class Woman extends Human{

    }

    public void sayHello(Human guy) {
        System.out.println("hello, guy!");
    }

    public void sayHello(Man guy) {
        System.out.println("hello, gentleman!");
    }

    public void sayHello(Woman guy) {
        System.out.println("hello, lady!");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sd = new StaticDispatch();
        sd.sayHello(man);	//輸出 hello, guy!
        sd.sayHello(woman); //輸出 hello, guy!
    }
}
           

上述代碼輸出結果為:

Java虛拟機:靜态分派和動态分派重寫和重載靜态分派和重載動态分派與重寫

Human man = new Man();

上述代買“Human”是變量的 “靜态類型” (Static Type), 或者稱 “外觀類型” (Apparent Type),後面的 “Man” 則被稱為變量的 “實際類型” (Actual Type) 或者叫 “運作時類型” (Runtime Type)。

// 無效變化
man = (Man) man;
sd.sayHello(man); 		// 輸出hello,guy!

// 靜态類型變化
sd.sayHello((Man) man);		// 輸出hello, gentleman!
sd.sayHello((Woman) woman);	// 輸出hello, lady!
//sd.sayHello((Woman) man);   // 異常,Man不能強制轉為(Woman)

// 實際類型變化
man = new Woman();
sd.sayHello(woman);     //輸出hello, guy!

// 靜态類型變化
sd.sayHello((Woman) man); //輸出hello, lady!
           

上述代碼human的靜态類型是Human, 在sayHello()方法中使用強制轉型可以 臨時 改變這個類型,這個改變是在編譯期可知的。

// 無效變化
man = (Man) man;
sd.sayHello(man); 		// 輸出hello,guy!
           

之是以這一步是無效變化,是因為虛拟機(準确地說是編譯器)在重載時是通過參數的靜态類型而不是實際類型作為判定依據的。也就是靜态分派發生在編譯階段。

通過運作javap 反彙編位元組碼檔案,我們看到如下:

Java虛拟機:靜态分派和動态分派重寫和重載靜态分派和重載動态分派與重寫
  • 34行至41行是
// 無效變化
man = (Man) man;
sd.sayHello(man); 		// 輸出hello,guy!
           
  • 44行至49行是
// 靜态類型變化
sd.sayHello((Man) man);		// 輸出hello, gentleman!
           
  • 很顯然,38: astore_1之後,man的靜态類型依然是Human, 并且sayHello方法裡又沒進行強制轉型,是以仍然輸出hello,guy!。

重載方法比對優先級

見《深入了解java虛拟機(第3版)》P306 - P307

簡而言之,重載方法比對如果找不到對應的參數類型的重載方法,則可以

  • 優先級最高:自動類型轉換(一次或多次) 例如:
void sayHello(int args){
}

void sayHello(long args){
}
           
sayHello('c'),會調用sayHello的int方法,注釋掉該方法後,則會調用long方法
優先級為 char > int > long > float >double
也就是精度從低到高,高精度參數不能調用低精度參數類型的方法。
即 char 不能比對到 byte 和 short 類型的重載, byte精度小,而short是16位
有符号整數,而char是16位無符号整數,這種轉型是不安全的。
           
  • 優先級其次:如果沒有能夠自動類型轉換,就可以自動裝箱, 即可以參數包裝成它的封裝類型。如’c’可以包裝成java.lang.Character類型。
  • 優先級第3,如果沒有封裝類型的方法,還可以找到裝箱類所實作的接口類型,進行又一次自動轉型。如果同時出現兩個參數為裝箱類所實作的接口類型的方法,此時編譯器無法确定要自動轉型為哪種類型,就會提示“類型模糊” (Type Ambiguous),并拒絕編譯。
  • 優先級第4,裝箱後轉型為父類。‘c’裝箱成Character然後調用Character的父類Object的方法。
  • 最後,變長參數的重載優先級是最低的。void sayHello(char… args){},且無法轉型為int調用void sayHello(int… args){}。

動态分派與重寫

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");
        }

        private void Goodbye() {
            System.out.println("man say goodbye");
        }

        public void sayGoodbye(){
            Goodbye();
        }
    }

    static class Woman extends Human{
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }

    static class ManChild extends Man{
    	// 不屬于方法重寫
    	// 無法直接通路父類的私有方法,也就不是重寫了
        private void Goodbye() {
            System.out.println("manChild say goodbye");
        }

        public void sayGoodbye(){
            super.Goodbye();
            Goodbye();
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        Human manChild = new ManChild();
        man.sayHello();     // man say hello
        woman.sayHello();   // woman say hello
        manChild.sayHello();// man say hello

        Man man2 = new Man();
        ManChild manChild2 = new ManChild();
        man2.sayGoodbye();  // man say goodbye
        
        // man say goodbye
        // manChild say goodbye
        manChild2.sayGoodbye(); 
    }
}
           

重寫與動态分派有密切的關聯,動态分派根據實際類型,方法調用按照繼承關系從下往上。

字段永遠不會參與多态

public class FieldHasNoPolymorphic {
    static class Father{
        public int money = 1;

        public Father() {
            money = 2;
            showMeTheMoney();
        }

        public void showMeTheMoney(){
            System.out.println("Father has $" + money);
        }
    }

    static class Son extends Father{
        public int money = 3;

        public Son() {
            money = 4;
            showMeTheMoney();
        }

        public void showMeTheMoney(){
            System.out.println("Son has $" + money);
        }
    }

    public static void main(String[] args) {
        // Son has $0
        // Son has $4
        Father guy = new Son();

		// Son has $4
        guy.showMeTheMoney();
        // This guy has $2
        System.out.println("This guy has $" + guy.money);
    }
}
           

Son 類在建立時,會先隐式調用Father的構造函數,而Father構造函數中對showMeTheMoney()的調用是一次虛方法調用,實際執行的版本是Son::showMeTheMoney()方法。通路的是Son的money字段,此時子類已經被加載但還沒初始化,是以結果自然是0。初始化時再列印出了"Son has $4"。 父子類中,方法調用是按照繼承關系從下往上,是以guy.showMeTheMoney(); 調用的是子類的方法。

而字段永不參與多态,是以guy.money通過靜态類型通路到了父類中的money。

靜态分派是多分派,動态分派是單分派