Java炸彈:重載、重寫、隱藏、遮蔽、遮掩

        看《重構(註釋版)》中「封裝集合(Encapsulate Collection)」一節時,因爲該重構手法對於不一樣的 Java 版本會有相對應不一樣的處理方式,因而註釋者在旁邊給出提示:Java 2 中的新 Collections API 主要是由《Java 解惑》、《Effective Java》這兩本書的做者開發改進的。我想,這可真是一個大消息,Java 類庫的開發者所寫的書必定得看,開發者確定深刻探尋過 Java 內部機制,說不定能從書中得到未知的知識點呢。java

        好在我記得本身電腦裏下載過《Java 解惑(中文版)》的清晰電子版,因而改變路線,看起這本書來了。真是不看不知道,一看嚇一跳!裏面有 95 個 Java 謎題,一點點地看了以後,我是既慚愧又興奮,由於裏面的題真的是「莫名其妙」,直白點就是:十有八九是想都想不出來的,⊙﹏⊙b…想要真正掌握 Java ,我以爲《Java 解惑》是不得不看的。大多謎題可謂是前所未見的炸彈,一不當心就 over 了。編程

        快點切入正題,如文章標題所示,我從《Java 解惑》中彷佛認識到了幾個名詞,之因此用彷佛修飾,由於重載、重寫、隱藏這幾個接觸過了,而遮蔽、遮掩或許是見過但也就忘光了。就整理一下文中的一些與此相關的 Java 謎題用本身的理解描述一下吧。數組

        重載(overload):同一個類中名字相同但參數列表不一樣的多個方法之間的關係。ide

        關於重載,是咱們比較熟悉的了,最多見的就是運用在類的多個構造函數中,看一下 Java 幫助文檔,就能夠明白這一狀況了。而在《Java 解惑》中,做者給出了下面一個謎題:模塊化

public class Confusing {    
    private Confusing(Object o) {   
        System.out.println("Object");   
    }   
    private Confusing(double[] dArray) {   
        System.out.println("double array");   
    }   
    public static void main(String[] args) {   
        new Confusing(null);   
    }   
}

         問此時 main() 中將會輸出什麼?初初一看,並無多分析就以爲應該是輸出「Object」,雖然Java中的數組實際上也是引用類型,但畢竟Object 是全部類的最終父類,並且目前 JDK 就連參數中的基本數據類型變量也能夠被自動想上轉型成包裝類而成爲 Object 的子類。因而我保守一點地就認爲參數 null 應該是匹配到 Object 那個重載方法去了。函數

        但是這答案是錯的,JVM 對於重載方法的解析是這樣的:先找出方法名匹配的全部可能的方法;而後根據傳進來的形參再次篩選出可能的重載方法;最後纔是在這些方法中匹配到一個最精確的一個方法。因而,上面的那個謎題就變成肯定哪個纔是最精確這一點子上了。學習

          而關於如何判斷最精確,有這樣的機制:若是某個重載方法可以接收全部傳遞給另外一個重載方法的實參類型,那麼對於參數列表來看,顯而後者至少是前者的子集,固然也就更精確了。this

           回到謎題上來,Confusing(Object)能夠接受任何傳遞給 Confusing(double[ ])的參數(任何數組引用最終可以都是 Object 對象),所以 main() 中的 null 應該是被 JVM 匹配到 Confusing(double[ ]) 中,也就有了與我所認爲的結果相反的輸出了。spa

           小結:這個謎題代表了咱們在寫重載方法時,最好是明確地區分出各個方法中的參數列表,不要讓彼此之間有互相包含、模糊不清的關係。雖然重載是爲了在相同名字的方法中傳入實參,由 JVM 動態解析選擇合適的方法,但有時也很容易陷入這種方便背後所帶來的地雷區當中。其中一種可行的辦法就是,提供不一樣的方法名。可是構造函數的名字必定得相同的啊?設計

        實際上,在《重構與模式》第六章中,做者用他自身的項目經驗對「建立」這一話題展開了講解,就算是構造函數,也有很好的重構手法將其清晰地區分開來,不使用重載而是用不一樣名稱的方法,將本來須要重載的構造函數委託給具備最大完整參數列表的私有構造函數中。又是一本經典,值得看哦…

        重寫(override):父類中的實例方法被其子類從新實現。既然是實例方法,那就是非 static 修飾的了,不然就是 static 靜態方法了,那叫作類方法。在我看來,正是重寫這一機制的存在,才爲多態機制提供了基礎。或許 implements (實現)一個 interface (接口)中所聲明的方法也能成爲重寫,由於 interface 的一部分存在緣由也是爲了多態。

        對於重寫,在《Java 解惑》中有下面這個謎題讓我明白:絕對不能在構造函數中調用可能會被子類重寫的方法。

class Point {  
    protected final int x, y;  
    private final String name;  
    Point(int x, int y) {  
        this.x = x;  
        this.y = y;  
        name = makeName();  
    }  
    protected String makeName() {  
        return "[" + x + "," + y + "]";  
    }  
    public final String toString() {  
        return name;  
    }  
}  

public class ColorPoint extends Point {  
    private final String color;  
    ColorPoint(int x, int y, String color) {  
        super(x, y);  
        this.color = color;  
    }  
    protected String makeName() {  
       return super.makeName() + ":" + color;  
    }  
    public static void main(String[] args) {  
        System.out.println(new ColorPoint(4, 2, "purple"));  
    }  
}

        此時程序運行結果並非咱們所想的 [4,2]:purple ,而是 [4,2]:null 。爲何會這樣?看看下面用流程標號註釋過的代碼,就能理解了。

class Point {  
    protected final int x, y;  
    private final String name;  
    Point(int x, int y) {  
        this.x = x;  
        this.y = y;  
        name = makeName();// 3. 因爲被子類重寫過的makeName()  
    }  
    protected String makeName() {  
        return "[" + x + "," + y + "]";  
    }  
    public final String toString() {  
        return name;  
    }  
}  
 
public class ColorPoint extends Point {  
    private final String color;  
    ColorPoint(int x, int y, String color) {  
        super(x, y); // 2. 調用Point父類構造函數  
        this.color = color; // 5. 初始化 color ,但是已經太晚了...  
    }  
    protected String makeName() {  
        // 4. 問題來了:它在子類構造函數以前調用了  
        // 而此時的 color 是 null 的啊!!!  
        return super.makeName() + ":" + color;  
    }  
    public static void main(String[] args) {  
        // 1. 調用ColorPoint子類構造函數  
        System.out.println(new ColorPoint(4, 2, "purple"));  
    }  
}

        思路很清晰了,ColorPoint 子類中的構造函數中的 this.color = color; 還未被執行到就將 null 做爲 String color 的值了。正是由於這種來來回回的調用使得程序變得不正常了,在我看來,有那麼一點相似於「回調」的意思。

        要去除這種代碼結構的不合理,最好仍是把 Point 父類構造函數中調用 makeName() 方法一句去掉,而後在 toString  中判斷並調用 makeName() 來爲 name 初始化,以下:

        小結:重寫對於多態當然重要,可是設計出不正確的代碼結構的話,本來想要的多態就會被扭曲甚至形成反效果。因而,絕對不要在構造函數中調用可能會被子類重寫的方法。

        好像文字太多的文章看了容易令人暈乎乎的,囉囉嗦嗦、模模糊糊地才寫了兩個詞兒,仍是分開來寫吧。其實,看了一部分《Java 解惑》才明白還有好多好多 Java 裏面該注意的要點。要想在適當的時候辨清各類語法上、機制上的知識點,難啊!

        記得高中語文課上讀過一片抒情的散文,標題爲「哦——香雪!」而我看了《Java 解惑》,想說「噢——Java!」~~~~(>_<)~~~~

總結:

一、在咱們編程領域,好書真的是一大把,就看本身有沒時間、有沒策略地去吸取了;

二、有時候看好書時留意做者對其餘書籍的「友情連接」,或者出版社推薦的相關書籍,這樣就可以免去本身慢慢搜尋好書的過程了,O(∩_∩)O哈!

        隱藏(hide):子類的某個字段、靜態方法、成員內部類與其父類的具備相同名字(對於靜態方法還須要相同的參數列表),此時父類對應的字段、靜態方法、成員內部類就被隱藏了。

        舉個例子,天鵝(Swan)是會飛的,而醜小鴨(UglyDuck)小時候是不會飛的,看看下面的代碼,看看可以打印出什麼。

class Swan {  
    public static void fly() {  
        System.out.println("swan can fly ...");  
    }
}  
class UglyDuck extends Swan {     
    public static void fly() {  
        System.out.println("ugly duck can't fly ...");  
    } 
}  
public class TestFly {
    public static void main(String [] args) {  
        Swan swan = new Swan();  
        Swan uglyDuck = new UglyDuck();  
        swan.fly();  
        uglyDuck.fly();  
    }  
}

        按道理的話,咱們認爲應該是輸出兩句不一樣的結果,由於咱們可能認爲 UglyDuck 繼承了 Swan 而且「重寫」了 fly() 方法,並且在 main() 方法中 Swan uglyDuck = new UglyDuck();  也代表了 uglyduck 其實是 UglyDuck 類型的,所以構成了多態行爲。

        其實,運行結果是兩句「swan can fly ...」,爲何會這樣子?緣由有下:

        一、父類 Swan 中的 static 靜態方法 fly() 是不能被重寫的,上一段我對重寫二字用了雙引號;

        二、儘管子類 UglyDuck 中的 fly() 方法與父類中的有一致的參數列表,可是對於 static 方法來講,這叫隱藏(hide),而不是重寫(override);

        三、對於 static 方法,根本不存在像多態那樣的動態分派機制,JVM 不會根據對象引用的實際類型來調用對應的重寫方法。這一點在個例子中是最重要的。

        對於 static 方法,咱們稱之爲類方法,不是實例方法,對 static 方法的調用直接用所屬類名加個點就行,如 UglyDuck.fly() 。而實例方法就不得不使用對象引用來得到其可訪問方法的調用權。在上面的例子 main() 中的 uglyDuck.fly() 語句,JVM 根本據不會去判斷 uglyDuck 引用的到底是什麼類型,既然調用的是 fly() 方法,那麼 JVM 只會根據 uglyDuck 的聲明類型(即 Swan 類)去得到該 static 方法的調用。根本就談不上多態…

        這就說明,最好避免用對象引用的方式來訪問一個 static 方法。此外,別覺得在繼承關係上的父類、子類只要方法名、參數列表一致就是重寫(override)而構成多態,其實還得看看父類中的方法有沒有被什麼修飾符聲明(在這個例子中是 static 修飾的)。再如 final 修飾符的方法則代表不可被子類重寫,即方法名、參數列表不能和父類徹底一致。在我看來,這一類修飾符就代表了方法、變量、字段等特有的性質,或者是身份。

        對於隱藏(hide),其實是爲了使得父類中的該方法、字段、內部類等不容許再被下一級繼承樹的子子類所繼承。提及隱藏,我想起《代碼大全 2》當中恰好看過的內容,做者認爲把握住信息隱藏的原則來思考軟件構建要優於面向對象原則。有點抽象難懂,書中還講到封裝、模塊化和抽象等幾個概念,建議看看,我也要回過頭去多啃啃這些抽象概念。

        要修改上面代碼,只須要去掉兩個 static 則可,那就構成多態了。《Java 解惑》中其餘謎題還講到多種該注意的地方,能夠看看。

小結:

        一、注意掌握重寫(override)與隱藏(hide)的異同點:相同點就是二者都是相對於繼承樹中父類、子類來講,而不一樣點就是其目的以及所形成的效果。別把重寫和隱藏混淆在一塊兒了;

        二、對於 static 方法,要避免用具體的對象引用來調用,而應該簡單的用其所屬類名進行調用便可。

        遮蔽(shadow):其實就是平時咱們可能遇到的窄做用域的變量名、方法名、類名等將其餘相同名字的變量、方法、類屏蔽掉的現象。

        例如,最多見的就是局部變量將類實例變量屏蔽了。其實,遮蔽這個詞我以前好像也沒什麼印象,不過做用域屏蔽這種狀況咱們大多應該會避免的了,由於課堂上、教材上對於變量做用域的內容已經講解過了,儘管沒有這麼一個術語。此時若是想要得到被遮蔽實體的引用、調用,則只能經過完整的限定名去實現了。不過有一些狀況多是根本就引用不到的,被屏蔽得太嚴密了。

        遮掩(obscure):一個變量能夠遮掩具備相同名字的一個類,只要它們都在同一個範圍內:若是這個名字被用於變量與類型都被許可的範圍,那麼它將引用到變量上。類似地,一個變量名或一個類名能夠遮掩一個包。遮掩是惟一一種兩個名字位於不一樣的名字空間的名字重用形式,這些名字空間包括:變量、包、方法或類。若是一個類型或一個包被遮掩了,那麼你不能經過其簡單名引用到它,除非是在這樣一個上下文環境中,即語法只容許在其名字空間中出現一種名字。遵照命名習慣就能夠極大地消除產生遮掩的可能性。

        其實,遮掩這個術語我更是徹底沒聽過了,上面這一段是從《Java 解惑》中引用過來的。我以爲,若是代碼是一我的所寫,或者團隊中你們都遵照了必定的命名規範,並且也各自分配了必定職責,那麼遮掩這種狀況應該是能夠避免的。一樣,須要使用徹底限定名來引用被遮掩掉的實體,以下:

        用前面例子的代碼大概就是這種狀況:

public class TestFly {    
    // 如此變量名  
    static String System = "system";  
    public static void main(String [] args) {  
	    //String System = "hao";    
	    // 編譯不經過  
	    //System.out.println("No");  
	    // 編譯經過  
	    java.lang.System.out.println("OK");  
    }  
}

小結:

        一、我以爲,在文章標題當中的五個名詞當中,尤其前面三個最爲重要,陷阱炸彈也多多,並且文中所講僅僅是那麼一丁點兒相關的,大量更細節的還得慢慢發現;

        二、就算後面兩個名詞,也就是這兩種狀況不常見,但瞭解一下記在腦裏仍是不錯的,畢竟爲本身增長了專業詞彙量;

        三、最後,就是建議各位也看看《Java 解惑》而後也告訴我一些炸彈型陷阱之類的,呵呵...學習快樂!加油!

相關文章
相關標籤/搜索