Java中的過載和覆蓋的細微差別 - rajivprab
我已經用Java程式設計超過五年了,並且認為我知道過載和覆蓋是如何工作的。只有一次我開始思考並寫下以下的角落案例,我才意識到我幾乎不知道它。為了遊戲化這些細微差別,我在下面將它們列為一系列謎題。
單一分發
假設有以下類:
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; // What gets printed? 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中物件導向程式設計的教科書示例。被呼叫的方法取決於“實際”例項型別,而不是“宣告的”例項型別。即,無論您將變數定義為一個Child還是Parent,因為實際的例項型別是Child,Child::print都將被呼叫。
第二套列印更加棘手。stringObject和string都是完全相同的字串。唯一的區別是string宣告為一個String型別,而stringObject宣告為一個Object型別。Java不支援雙分發排程,因此,在處理方法引數時,重要的是引數的“宣告”型別,而不是其“實際”型別。print(Object)將被呼叫,即使“實際”引數型別是String。
隱藏覆蓋
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" |
Java在檢查子類覆蓋之前,首先會選擇要呼叫的方法。在這種情況下,宣告的例項型別是Parent,唯一匹配的方法是Parent::print(Object)。當Java檢查任何潛在的覆蓋Parent::print(Object)的方法時,它沒有找到任何覆蓋方法,因此這隻能是它的執行的方法。
暴露覆蓋
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!" |
這和前面的例子之間的唯一區別是我們新增了一個新Parent::print(String)方法。這個方法實際上永遠不會被執行 - 如果它被執行它將丟擲異常!
java執行時找到了匹配的Parent::print(String)方法,然後看到此方法被覆蓋Child::print(String)。
模糊引數
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; // What gets printed? 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); // Does not compile |
與第一個單分發single_dispatch示例類似,此處重要的是引數的宣告型別,而不是實際型別。此外,如果有多個方法對給定引數同樣有效,則Java會丟擲編譯錯誤並強制您指定應呼叫哪個方法。
多重繼承 - 介面
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(); |
與前面的示例類似,這也不編譯。具體來說,類定義本身Child將無法編譯,因為在Father和中存在衝突的預設方法Mother。您需要更新Child類以指定其行為Child::print。請參閱此處以獲取更詳細的說明。
多重繼承 - 類和介面
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(); // Prints: "I am a class!" |
說明: 上一節中的連結文章實際上也涵蓋了這一點。如果類和介面之間存在繼承衝突,則類獲勝。
傳遞覆蓋
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(); // Prints: "I am Child!" |
覆蓋方法即使對傳遞呼叫也會生效。有人可能會認為Parent::print總會呼叫Parent::foo。但是如果方法被覆蓋,那麼Parent::print將呼叫被覆蓋的版本foo()。
私有覆蓋
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(); // Prints: "I am Parent!" |
除了與前一個一個區別外,其餘相同。Parent.foo()現在被宣佈為私有。
通常假設將方法從公共更改為私有,只要編譯仍然成功,就是純粹的重構更改。上面的例子表明這是錯誤的 - 即使編譯成功,系統行為也會以戲劇性的方式發生變化。
通過@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不允許重寫靜態方法。如果在父類和子類中都定義了相同的靜態方法,則例項的實際型別根本不重要。只有宣告的型別用於確定呼叫兩個方法中的哪一個。
這與非靜態方法的情況完全相反,其中忽略宣告的型別以支援實際型別。因此,為什麼在將方法從非靜態更改為靜態或反之亦然時需要小心。即使沒有編譯錯誤,系統行為也可能發生巨大變化。
這是使用@Override註釋標記所有覆蓋方法的另一個原因。在上面的例子中,新增註釋時會出現編譯錯誤Child::print,告訴您由於它是靜態的,因此無法覆蓋該方法。
這也是為什麼永遠不要使用類的例項呼叫靜態方法的好習慣 - 它可能導致像上面這樣令人驚訝的行為,並且在進行有問題的重構更改時無法提醒您。許多像Intellij這樣的IDE會在從非靜態上下文中呼叫靜態方法時發出警告,最好跟進這些警告。
靜態連結
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 |
這是我們之前介紹過的一些不同概念的組合。對於例項方法,即使呼叫者在父級中,覆蓋也會生效。但是,對於靜態方法,即使變數的宣告型別是Child,Parent::staticMethod也會因為中間foo()方法而被呼叫。
總結
如果碰到其中一個,那就是非常棘手,繼承容易出錯。如果你想要聰明,有一天它會咬你的屁股。使用非常愚蠢的護欄和最佳實踐來保護自己:
- 始終使用@Override註釋標記所有覆蓋方法
- 始終使用類引用而不是例項引用來呼叫靜態方法
- 設定IDE警報或lint錯誤以強制執行上述和其他程式碼異味
- 使用組合而不是繼承
相關文章
- C++之過載覆蓋和隱藏C++
- 重寫、覆蓋、過載、多型幾個概念的區別分析多型
- Java中的過載和重寫Java
- 人工智慧的下一個重大挑戰:理解語言的細微差別人工智慧
- java覆蓋率檢測-jacocoJava
- 模型評估過程中:命中率/覆蓋率模型
- Java—重寫與過載的區別Java
- 過載和重寫的區別
- 軟體測試培訓之:白盒測試的語句覆蓋法和判定覆蓋法
- 面試官:Java的重寫和過載有什麼區別?面試Java
- Java 覆蓋率 Jacoco 插樁的不同形式總結和踩坑記錄Java
- resultMap 和 resultType 的欄位對映覆蓋問題
- java 中equals和==的區別Java
- 【譯】Visual Studio Enterprise 中的程式碼覆蓋率特性
- SOLIDWORKS 2023 工程圖的亮點新功能之一:材料明細表的覆蓋Solid
- MySQL 聚簇索引 和覆蓋索引MySql索引
- 矩形覆蓋
- Solr 18 - 通過SolrJ區域性更新Solr中的文件 (原子操作、非覆蓋操作)Solr
- 裸土未覆蓋AI識別系統AI
- 超市無線覆蓋的需求分析
- MySQL 的覆蓋索引與回表MySql索引
- [轉載] Java Challengers#1:JVM中的方法過載JavaJVM
- Java中 equals() 方法和 == 的區別Java
- Java中Vector和ArrayList的區別Java
- Java 程式碼覆蓋率調研報告Java
- 【Java】方法的過載Java
- [轉載] 整理下java中stringBuilder和stringBuffer兩個類的區別JavaUI
- 最小圓覆蓋
- Mysql索引覆蓋MySql索引
- 棋盤覆蓋
- 無線覆蓋解決方案需要考慮哪些細節
- 重名就會被覆蓋?那JavaScript中是如何實現過載的呢?JavaScript
- 介面自動化測試的覆蓋和 Diff 平臺的悖論
- 中興通訊:5G室內覆蓋白皮書(附下載)
- 程式碼覆蓋率與測試覆蓋率比較
- MySQL優化之覆蓋索引的使用MySql優化索引
- 洛谷 P11011 點的覆蓋
- 測試覆蓋率 之 Cobertura的使用