JAVA 編程五年多,我自以為已經(jīng)熟諳 Overload 和 Override 背后的工作機制。當開始思考和記錄下面這些案例時,才意識到我對它們的了解并不像自己想象的那樣。為了讓內(nèi)容更有趣,下面會把它們列為一系列謎題,同時也提供了答案。如果你能不偷看做出所有答案,我會對你刮目相看。
1. 單一分派
給定下面兩個類:
class Parent { void print(String a) { log.info("Parent - String"); } void print(Object a) { log.info("Parent - Object"); } } class Child extends Parent { void print(String a) { log.info("Child - String"); } void print(Object a) { log.info("Child - Object"); } }
下面代碼打印結果是什么?
String string = ""; Object stringObject = string; // 打印結果是什么? Child child = new Child(); child.print(string); child.print(stringObject); Parent parent = new Child(); parent.print(string); parent.print(stringObject);
答案:
child.print(string); // Prints: "Child - String" child.print(stringObject); // Prints: "Child - Object" parent.print(string); // Prints: "Child - String" parent.print(stringObject); // Prints: "Child - Object"
child.print(string) 和 parent.print(string) 是 Java 面向?qū)ο蟪绦蚓幊痰慕炭茣绞纠U{(diào)用的方法取決于實例的“實際”類型,而非“聲明”類型。也就是說,不論把變量定義為 Child 還是Parent,因為實際的實例類型是Child 都會調(diào)用 Child::print。
第二組輸出結果更為棘手。stringObject 和 string 是完全相同的字符串,唯一的區(qū)別在于 string 聲明為 String,而 stringObject 聲明為 Object。Java 不支持雙重分派(double-dispatch),因此在處理方法參數(shù)時,參數(shù)的“聲明”類型優(yōu)先于“實際”類型。即使參數(shù)“實際”類型是 String 還是會調(diào)用 print(Object)。
2. 隱式 Override
給定下面兩個類:
class Parent { void print(Object a) { log.info("Parent - Object"); } } class Child extends Parent { void print(String a) { log.info("Child - String"); } }
下面代碼打印結果是什么?
String string = ""; Parent parent = new Child(); parent.print(string);
答案:
parent.print(string); // Prints: "Parent - Object"
實例的“實際”類型是 Child,聲明的參數(shù)類型是 String。代碼中確實有一個 Child::print(String)方法。前面的例子中 parent.print(string) 調(diào)用的正是這個方法,但這里不是。
檢查子類 Override 前,Java 似乎要先選擇調(diào)用哪個方法。這種情況下,實例聲明的類型是Parent,而 Parent 中唯一匹配的方法是 Parent::print(Object)。接著,Java 會檢查Parent::print(Object) 有沒有潛在的 Override 方法,結果沒有找到。因此,最后執(zhí)行的就是這個方法。
3. 顯式 Override
給定下面兩個類:
class Parent { void print(Object a) { log.info("Parent - Object!"); } void print(String a) { throw new RuntimeException(); } } class Child extends Parent { void print(String a) { log.info("Child - String!"); } }
下面代碼打印結果是什么?
String string = ""; Parent parent = new Child(); parent.print(string);
答案:
parent.print(string); // Prints: "Child - String!"
這個例子與前面的唯一區(qū)別在于添加了一個新的 Parent::print(String) 方法。實際上這個方法從來沒有執(zhí)行。如果運行,會拋出一個異常。然而,正是因為它的存在讓 Java 執(zhí)行了一個不同的方法。
運行時在分析 parent.print(String) 的時候,找到了一個可以匹配的 Parent::print(String) 方法,然后看到這個方法被 Child::print(String) 覆寫。
通常認為如果不調(diào)用,只是添加一個新方法絕不會改變系統(tǒng)行為。上面的例子顯然是另一種情況。
4. 帶有歧義的參數(shù)
給定下面的類:
class Foo { void print(Cloneable a) { log.info("I am cloneable!"); } void print(Map a) { log.info("I am Map!"); } }
下面代碼打印結果是什么?
HashMap cloneableMap = new HashMap(); Cloneable cloneable = cloneableMap; Map map = cloneableMap; // 打印結果是什么? Foo foo = new Foo(); foo.print(map); foo.print(cloneable); foo.print(cloneableMap);
答案:
foo.print(map); // Prints: "I am Map!" foo.print(cloneable); // Prints: "I am cloneable!" foo.print(cloneableMap); // 編譯失敗
與單一分派類似,參數(shù)聲明的類型優(yōu)先于實際類型。如果傳入?yún)?shù)有多個方法可以匹配,Java 會拋出編譯錯誤,要求明確指定調(diào)用哪個方法。
5. 多重繼承:接口
給定下面的接口:
interface Father { default void print() { log.info("I am Father!"); } } interface Mother { default void print() { log.info("I am Mother!"); } } class Child implements Father, Mother {}
下面代碼打印結果是什么?
new Child().print();
與前面的例子類似,這個示例也不能編譯通過。具體來說,由于 Father 和 Mother 中存在互相沖突的默認方法,Child 類無法編譯通過。需要在 Child 類定義中明確指定 Child::print。
6. 多重繼承:類與接口
給定下面的類和接口:
class ParentClass { void print() { log.info("I am a class!"); } } interface ParentInterface { default void print() { log.info("I am an interface!"); } } class Child extends ParentClass implements ParentInterface {}
下面代碼打印結果是什么?
new Child().print();
答案:
new Child().print(); // Prints: "I am a class!"
說明:如果類和接口在繼承時發(fā)生沖突,繼承類優(yōu)先。
7. Override 傳遞
給定下面的類:
class Parent { void print() { foo(); } void foo() { log.info("I am Parent!"); } } class Child extends Parent { void foo() { log.info("I am Child!"); } }
下面代碼打印結果是什么?
new Child().print();
答案:
new Child().print(); // Prints: "I am Child!"
Override 對傳遞調(diào)用也有效。看過 Parent 定義的人可能認為 Parent::print 總是調(diào)用Parent::foo。但是如果該方法被 Override,那么 Parent::print 將調(diào)用 Override 版本的 foo()。
8. 私有 Override
給定下面的類:
class Parent { void print() { foo(); } private void foo() { log.info("I am Parent!"); } } class Child extends Parent { void foo() { log.info("I am Child!"); } }
下面代碼打印結果是什么?
new Child().print();
答案:
new Child().print(); // Prints: "I am Parent!"
現(xiàn)在 Parent.foo() 聲明變?yōu)?private,除此之外,這個示例與前面的例子完全相同。無論 foo() 在子類中是否實現(xiàn)了其它版本,也不管調(diào)用 print() 的實例類型如何,當 Parent.print() 調(diào)用 foo()時,foo() 都會硬編碼為 Parent.foo()。
通常認為,把一個方法從 public 改為 private,只要編譯成功,僅僅是一次重構。上面的例子證明這種想法是錯的:即使編譯成功,系統(tǒng)行為也可能出現(xiàn)意料之外的變化。
解決方法,可以為方法加上 @Override 注解。一旦改變基類方法的可見性,就會引起編譯錯誤。
9. 靜態(tài) Override
給定下面的類:
class Parent { static void print() { log.info("I am Parent!"); } } class Child extends Parent { static void print() { log.info("I am Child!"); } }
下面代碼打印結果是什么?
Child child = new Child(); Parent parent = child; parent.print(); child.print();
答案:
parent.print(); // Prints: "I am Parent!" child.print(); // Prints: "I am Child!"
Java 不支持靜態(tài)方法 Override。如果父類和子類中定義了相同的靜態(tài)方法,僅根據(jù)聲明類型決定調(diào)用哪個方法,不考慮實例類型。
這與非靜態(tài)方法的處理完全相反:非靜態(tài)方法會依據(jù)實例類型決定調(diào)用哪個方法,忽略聲明類型。因此,把方法改為靜態(tài)方法或者反向操作需要格外小心。即使沒有編譯錯誤,系統(tǒng)行為也可能出現(xiàn)意料之外的變化。
這也是給方法加上 @Override 注解的另一個重要原因。上面的例子中,如果為 Child::print 加上注解會報告一個編譯錯誤:靜態(tài)方法不能 Override。
不僅如此,最佳實踐也要求不使用實例調(diào)用靜態(tài)方法:不僅可能導致類似上面的意外結果,在重構出現(xiàn)問題時也無法告警。示例中這樣調(diào)用 static 方法時,像 Intellij 這樣的 IDE 會給出警告,最好給予足夠的重視并處理。
10. 靜態(tài)鏈接
給定下面的類:
class Parent { void print() { staticMethod(); instanceMethod(); } static void staticMethod() { log.info("Parent::staticMethod"); } void instanceMethod() { log.info("Parent::instanceMethod"); } } class Child extends Parent { static void staticMethod() { log.info("Child::staticMethod"); } void instanceMethod() { log.info("Child::instanceMethod"); } }
下面代碼打印結果是什么?
Child child = new Child(); child.print();
答案:
Parent::staticMethod Child::instanceMethod
這個例子組合了之前討論的各個主題。即使調(diào)用方法在父類中,Override 也會生效。然而,對于靜態(tài)方法,即使變量聲明的類型是 Child 仍然調(diào)用的是 Parent::staticMethod。
11. 總結
如果說有什么值得注意的話,那就是處理繼承非常棘手,而且很容易出錯。如果試圖在繼承上耍小聰明,總有一天會吃苦頭。還是建議循規(guī)蹈矩,用最佳實踐來保護自己:
- 用@override 注解標記所有 Override 方法
- 始終用類而非實例調(diào)用靜態(tài)方法
- 在 IDE 中設置警告或者 lint 錯誤,強制執(zhí)行上面的規(guī)則檢查有問題的代碼
- 能用組合就不要繼承