我們先來看一段經典的代碼示例,預測一下它的輸出結果:
class Dog {
public static void bark() {
System.out.print("woof ");
}
}
class Basenji extends Dog {
public static void bark() { }
}
public class Bark {
public static void main(String args[]) {
Dog woofer = new Dog();
Dog nipper = new Basenji();
woofer.bark();
nipper.bark();
}
}
乍看之下,似乎程序應該只輸出一個 "woof"。畢竟 Basenji 繼承自 Dog,並將 bark 方法定義為空實現。main 方法分別在 Dog 類型的 woofer 和 Basenji 類型的 nipper 上調用了 bark。按理說巴辛吉犬不會吠叫,但實際運行程序會發現輸出的是 "woof woof"。這究竟是怎麼回事?
問題的癥結在於 bark 是一個 靜態方法。根據 Java 語言規範(JLS 15.12.4.4),靜態方法的調用 不存在動態分派機制。當程序調用靜態方法時,要執行的方法在 編譯期 就已確定,依據是調用表達式中點號左側變量的 編譯期類型(靜態類型)。
在這個例子中,woofer 和 nipper 都被聲明為 Dog 類型。因為它們擁有相同的編譯期類型,編譯器會讓兩者調用同一個方法:Dog.bark。這就是為什麼程序輸出 "woof woof" 的原因——儘管 nipper 的運行期類型是 Basenji,編譯器卻只關心其聲明類型。
子類中定義與父類同名、同參數的靜態方法,稱為 方法隱藏,而非重寫。隱藏不具備多態性,無法實現動態分派。若想獲得多態行為,必須移除 static 修飾符,讓方法成為實例方法,從而實現真正的 重寫。
class Dog {
public void bark() { // 移除 static
System.out.print("woof ");
}
}
class Basenji extends Dog {
@Override
public void bark() { } // 真正的重寫
}
修改後,nipper.bark() 將動態分派到 Basenji.bark(),程序輸出變為單個 "woof"。這也再次印證:重寫帶來動態分派,隱藏則無此特性。
@Override 註解。類名.方法名() 方式調用,而非通過實例引用,以提高代碼可讀性。