Java Puzzlers(5)更多類之謎

本章更多討論了子類對父類的繼承可能導致的各種陷阱,比如隱藏(hidden),遮蔽(shadow),遮掩(obscure),覆寫(override),重載(overload)等行爲。

1。首先來看看一個隱藏的例子:

class Base {
    public String className = "Base";
}

class Derived extends Base {
    private String className = "Derived";
}

public class PrivateMatter {
    public static void main(String[] args) {
        System.out.println(new Derived().className);
    }
}

我們可能指望它打印Base,可很抱歉,此程序是無法編譯通過,剛一看錯誤信息你可能愣住:

無權訪問private的className。。。

對於實例方法,子類可以對父類的實例方法進行覆寫,可對於實例變量(而且包括類變量,靜態方法,final靜態變量),子類只能隱藏父類中的相同名稱的變量,而不是覆寫。根據new Derived()的編譯期類型爲子類Derived,調用子類的className屬性,可此屬性是聲明爲private的,所以無法編譯通過。如果我們想調用父類中被隱藏的className,可以通過向上轉型來實現:

System.out.println(((Base)new Derived()).className);

此例告訴我們,JAVA語言中子類定義與父類相同類型和名稱的變量、嵌套類型和靜態方法,都將隱藏掉父類中相應的方法,而不是覆寫,所以,請避免隱藏!此過程中當然也不存在所謂多態。另外,我們不應該違反這樣一條規則:對基類所做的任何行爲,都應當可以同樣作用於子類。此例中子類className爲private,違反了基類中className是public的定義,這樣的寫法應該避免。

 

2。也許哪一天你突然想自己寫一個String來代替java.lang中的String,讓我們來看看會發生什麼?

public class StrungOut {
    public static void main(String[] args) {
        String s = new String("Hello world");
        System.out.println(s);
    }
}

class String {
    private final java.lang.String s;

    public String(java.lang.String s) {
        this.s = s;
    }

    public java.lang.String toString() {
        return s;
    }
}
試運行此程序,JVM會給你一個非常奇怪的消息:

StrungOut dose not have a main method!

怪了,明明有個main方法啊??請注意,main方法中的參數String []args,其中的String類型要求是java.lang.String,可JVM會自動地把把這些參數認爲是我們自定義的下面那個String類型,這就是錯誤的原因所在。教訓:避免重用類名,特別是java平臺的類型,特別是java.lang包下的類名!在此例中,當前類所在包中的所有類的main方法都將因此失效。

3。遮掩(obscure):我覺的翻譯成模糊也許更好。看看下面的例子:

public class ShadesOfGray {
    public static void main(String[] args){
        System.out.println(X.Y.Z);
    }
}

class X {
    static class Y {
        static String Z = "Black";
    }

    static C Y = new C();
}

class C {
    String Z = "White";
}

你認爲他應該打印什麼呢??黑還是白?還是黑白不分:),光明的力量總是偉大,它一直打印的是:white。這說明了X.Y一直調用的是靜態變量Y,而不是靜態內隱類Y。JAVA語言規範告訴我們,當一個變量和一個類型具有相同的名字,明確他們位於相同的作用範圍內,變量名具有優先權,同樣,變量名與類型名將遮掩包名。

即變量名>類型名>包名。其實上面的例子有更嚴重的問題,它並沒有遵循標準的JAVA命名習慣,變量應該以小寫開頭(mixedCase的格式),類名以大寫開頭(MaxedCase的格式),如果完全遵照習慣來寫,就不會出現此問題了。所以,請遵守標準的命名習慣。退一步,假設在某些情況下我們只能以此方式書寫,那我們怎麼訪問靜態內隱類Y呢?兩種方法:

System.out.println(((X.Y)null).Z);   //藉助表達式訪問類變量,還記的嗎?

在JDK5中還可以這樣:

public static <T extends X.Y> void main(String args[]){     //繼承X.Y類解決此問題。

            System.out.println(T.Z);

 }   

 

4。包A中的某個類被另一個包C中的子類所繼承,如果子類當中“覆寫”了父類中的方法,而此方法在父類中不是聲明爲public或者protected,那麼這並非覆寫,這兩個方法將沒有任何關係。所以,如果你希望某個類的一個方法被包外的子類所覆寫,請把此方法聲明爲protected或者public。

5。遮蔽(shadow),這裏討論了JDK5靜態導入需要注意的問題,看下面的例子:

import static java.util.Arrays.toString;

class ImportDuty {
    public static void main(String[] args) {
        printArgs(1, 2, 3, 4, 5);
    }

    static void printArgs(Object... args) {
        System.out.println(toString(args));
    }
}
這個例子表面上看起來很正常,可事實上是無法編譯通過的,編譯器告訴我們,找不到恰當的toString()方法。這是爲何?我們明明已經導入了Arrays.toString方法了啊?應該打印:[1,2,3,4,5]纔對!這是因爲編譯器將首先在類ImportDuty的範圍內尋找toString方法,這個方法將從Object類繼承而來的toString()方法,它並不能接受參數Object []args!這就是原因所在,某個範圍內的成員對比於靜態導入的具有優先權。也就是類ImportDuty的toString()方法遮蔽了Arrays.toString(Object []args)方法。遮蔽與遮掩的區別在於,遮蔽的只能是同類型的名稱,而遮掩的是不同類型的(如變量名遮掩類名,類名遮掩包名)。慎重使用靜態導入。

6。最後一個謎題很重要哦,我現在才知道JDK5對條件操作符(a?b:c)已經有重大改變。試着分別在JDK1.4和JDK5中運行下面的程序:

import java.util.Random;

public class CoinSide {
    private static Random rnd = new Random();

    public static CoinSide flip() {
        return rnd.nextBoolean() ?
            Heads.INSTANCE : Tails.INSTANCE;
    }

    public static void main(String[] args) {
        System.out.println(flip());
    }
}

class Heads extends CoinSide {
    private Heads() { }
    public static final Heads INSTANCE = new Heads();

    public String toString() {
        return "heads";
    }
}

class Tails extends CoinSide {
    private Tails() { }
    public static final Tails INSTANCE = new Tails();

    public String toString() {
        return "tails";
    }
}
發現了嗎?在jdk1.4及以前版本當中,此程序無法通過,報錯:

incompatible types for ?: neither is a subtype of the other

說什麼第2個操作數和第3個操作數都不是另外一個子類。而在JDK5下這個程序將正常運行,隨機打印heads或者tails。這是因爲在JDK5以前,條件運算符要求:當第2個和第3個操作數是引用類型時,它們其中的一個必須是另外一個的子類。而例子中Heads和Tails都不是對方的子類,所以產生了上面的錯誤。而在JDK5中,這個條件放寬了,第2個和第3個操作數如果是引用那麼都是合法的,只不過其結果類型將是這兩種類型的最小公共超類。此例中Heads和Tails的超類向上追溯有CoinSide,Object,而CoinSide是他們的最小公共超類。

如果想在JDK5以前運行上面的程序,可以把第2或者第3操作數向上轉型爲他們的超類即可:

   public static CoinSide flip() {
        return rnd.nextBoolean() ?
            (CoinSide)Heads.INSTANCE : Tails.INSTANCE;
    }

另外一些謎題討論了對Object類中方法的覆寫問題,特別要注意不要覆寫變成了重載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章