一件私事

出自 《java puzzle》


在下面的程序中,子類的一個域具有與超類的一個域相同的名字。那麼,這個程序會打印出什麼呢?

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);
}
}

對該程序的表面分析可能會認爲它應該打印Derived,因爲這正是存儲在每一個Derived實例的className域中的內容。
更深入一點的分析會認爲Derived類不能編譯,因爲Derived中的className變量具有比Base中的className變量更具限制性的訪問權限。
如果你嘗試着編譯該程序,就會發現這種分析也不正確。該程序確實不能編譯,但是錯誤卻出在PrivateMatter中。

如果className是一個實例方法,而不是一個實例域,那麼Derived.className()將覆寫Base.className(),而這樣的程序是非法的。一個覆寫方法的訪問修飾符所提供的訪問權限與被覆寫方法的訪問修飾符所提供的訪問權限相比,至少要一樣多[JLS 8.4.8.3]。
因爲className是一個域,所以Derived.className隱藏(hide)了Base.className,而不是覆蓋了它[JLS 8.3]。對一個域來說,當它要隱藏另一個域時,如果隱藏域的訪問修飾符提供的訪問權限比被隱藏域的少,儘管這麼做不可取的,但是它確實是合法的。事實上,對於隱藏域來說,如果它具有與被隱藏域完全無關的類型,也是合法的:即使Derived.className是GregorianCalendar類型的,Derived類也是合法的。
在我們的程序中的編譯錯誤出現在PrivateMatter類試圖訪問Derived.className的時候。儘管Base有一個公共域className,但是這個域沒有被繼承到Derived類中,因爲它被Derived.className隱藏了。在Derived類內部,域名className引用的是私有域Derived.className。因爲這個域被聲明爲是private的,所以它對於PrivateMatter來說是不可訪問的。因此,編譯器產生了類似下面這樣的一條錯誤信息:
PrivateMatter.java:11: className has private access in Derived
System.out.println(new Derived().className);
^
請注意,儘管在Derived實例中的公共域Base.className被隱藏了,但是我們還是可以通過將Derived實例轉型爲Base來訪問到它。下面版本的PrivateMatter就可以打印出Base:
public class PrivateMatter {
public static void main(String[] args) {
System.out.println(((Base)new Derived()).className);
}
}
這說明了覆寫與隱藏之間的一個非常大的區別。一旦一個方法在子類中被覆寫,你就不能在子類的實例上調用它了(除了在子類內部,通過使用super關鍵字來方法)。然而,你可以通過將子類實例轉型爲某個超類類型來訪問到被隱藏的域,在這個超類中該域未被隱藏。
如果你想讓這個程序打印Derived,也就是說,你想展示覆寫行爲,那麼你可以用公共方法來替代公共域。在任何情況下,這都是一個好主意,因爲它提供了更好的封裝[EJ Item 19]。下面的程序版本就使用了這項技術,並且能夠打印出我們所期望的Derived:
class Base {
public String getClassName() {
return "Base";
}
}
class Derived extends Base {
public String getClassName() {
return "Derived";
}
}
public class PrivateMatter {
public static void main(String[] args) {
System.out.println(new Derived().getClassName());
}
}
請注意,我們將Derived類中的getClassName方法聲明成了public的,儘管在最初的程序中與其相對應的域是私有的。就像前面提到的那樣,覆寫方法的訪問修飾符與它要覆寫的方法的訪問修飾符相比,所具有的限制性不能有任何降低。
本謎題的教訓是隱藏通常都不是一個好主意。Java語言允許你去隱藏變量、嵌套類型,甚至是靜態方法(就像在謎題48所展示的那樣),但是你不能認爲你就應該去隱藏。隱藏的問題在於它將導致讀者頭腦的混亂。你正在使用一個被隱藏實體,或者是正在使用一個執行了隱藏的實體嗎?要避免這類混亂,只需避免隱藏。
如果一個類要隱藏一個域,而用來隱藏該域的域具有的可訪問性比被隱藏域更具限制性,就像我們最初的程序那樣,那麼這就違反了包容性(subsumption)原則,即大家所熟知的Liskov置換原則(Liskov Substitution Principle)[Liskov87]。這項原則敘述道,你能夠對基類所作的任何事,都同樣能夠作用於其子類。包容性是面向對象編程的自然心理模型的一個不可分割的部分。無論何時,只要違反了這項原則,就會對程序的理解造成困難。還有其它數種用另一個域來隱藏某個域的方法也會違反包容性:例如,兩個域具有不同的類型;一個域是靜態的而另一個域不是;一個域是final的而另一個域不是;一個域是常量而另一個域不是;以及兩個域都是常量但是它們具有不同的值。
對於語言設計者而言,應該考慮消除隱藏的可能性:例如,使所有的域都隱含地是私有的。如果這樣做顯得過於嚴苛,那麼至少應該考慮對隱藏進行限制,以使其遵守包容性原則。
總之,當你在聲明一個域、一個靜態方法或一個嵌套類型時,如果其名字與基類中相對應的某個可訪問的域、方法或類型相同,就會發生隱藏。隱藏是容易產生混亂的:違反包容性的隱藏域在某種意義上是特別有害的。更一般地講,除了覆寫之外,要避免名字重用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章