參考資料《深入了解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!
}
}
上述代碼輸出結果為:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL3EzNxMjMwQTM4IjMwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
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 反彙編位元組碼檔案,我們看到如下:
- 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。