類和介面是Java程式設計語言的核心,它們也是Java語言的基本抽象單元。Java語言提供了許多強大的基本元素,供程式猿來設計類和介面。
要想區別一個設計良好的模組與一個設計不好的模組,最重要的因素是,這個模組對於外部其他模組而言,是否隱藏了內部的資料和其他的實現細節。一個設計良好的模組會隱藏所有的實現細節,把它的API與實現清晰的隔離開來。然後,模組之間只通過它們的API進行通訊,一個模組不需要知道其他模組內部的工作情況,也就是所謂的封裝。
封裝之所以非常重要有許多理由,其中大多數理由都源於這樣一個事實:它可以有效地解除一個系統中各模組之間的耦合關係,使得這些模組可以被獨立地開發、測試、最佳化、使用、理解和修改。這樣可以加速系統開發的速度,因為這些模組可以被並行地開發。它也減輕了維護的負擔,因為程式員很快就可以理解這些模組,並且在調試它們的時候可以不傷害其他的模組。雖然封裝本身無論是對內還是對外,都不會帶來更好的效能,但是它使得有效效能調節成為可能。一旦一個系統已經完成,通過分析就可以知道哪些模組影響了系統的效能,那麼這些模組可以被進一步最佳化,而不會影響到其他模組的正確性。封裝可以提高軟體的可重用性,因為單獨的模組並不依賴於其他的模組,除了開發這些模組所使用的環境之外,它們在其他環境中往往是有用。最後,封裝也降低了構建大型系統的風險;即使整個系統並不成功,這些獨立模組也有可能是成功的。
Java程式設計語言提供了許多設施來協助坐到資訊隱藏。其中一個設施是存取控制機制,它決定了類、介面和成員的可訪問性。
一個實體的可訪問性是由該實體聲明所在的位置,以及該實體聲明中所出現的存取修飾詞公同決定的。正確地使用這些修飾符對於實現封裝是非常關鍵的。
經驗表明,你應該儘可能地使每一個類或成員不被外界訪問。換句話說,你應該使用時最低可能的、並且與該軟體的正確功能相一致的存取層級。
對於頂層的類和介面,它們只有兩種可能的存取層級:包級私人的和公有的。如果你聲明了一個具有public修飾符的頂層類或者介面,那麼它是公有的;否則,它將是包級私人的。如果一個類或者介面能被做成包級私人的,那麼它就應該被做成包級私人的。通過把一個類或介面做成包級私人的,它實際上成了這個包的實現的一部分,而不是該包匯出API的一部分;並且,在以後的發行版本中,你可以對它進行修改、替換,或者去除,而無需擔心會傷害到現有的客戶。如果你把它做成公有的,你就有義務永遠支援它,以保持相容性。
如果一個包級私人的頂層類或介面只是在某一個類的內部被用到,那麼你應該考慮使它成為後者的一個私人嵌套類。這樣可以進一步降低它的可訪問性,然而,這樣做不像”使一個不必要的共公有類成為包級私人的類”那樣重要,因為一個包級私人的類已經是這個包的實現的一部分,而不是其API的一部分。
對於成員有四種可能存取層級,下面按照可訪問性遞增的順序列出來:
私人的——只有在聲明該成員的頂層類內部才可以訪問這個成員
包級私人的——聲明該成員的包內部的任何類都可以訪問這個成員。在技術上,它被稱為”預設存取層級”,如果沒有為成員指定存取修飾詞的話,那麼它就具有這樣的存取層級。
受保護的——該成員聲明所在類的子類可以訪問這個成員,並且,該成員聲明所在的包內部的任何類也可以訪問這個成員。
公有的——任何地方都可以訪問該成員。
當你仔細地設計了一個類的公有API之後,接下去應該把所有其他的成員都變成私人的。只有當同一個包內的另一個類真正需要訪問一個成員的時候,你才應該去掉private修飾符,使該成員變成包級私人的。如果你發現自己經常要做這樣的事情,那麼你應該重新檢查你的系統設計,看看是否另一種分解方案所得到的類具有更好的分離特性,彼此之間耦合度更小。可以這樣說,私人成員和包級私人成員都是一個類實現中的一部分,並不會影響到其匯出的API。然而,如果這些域所在類是實現了Serializable介面,那麼這些域可能會被”泄露”其匯出的API中。
對於公有類的成員,當存取層級從包級私人變成保護層級時,會出現可訪問性的巨大增加。受保護的成員是一個類匯出API的一部分,必須永遠被支援。更進一步,一個匯出的類的每一個受保護的成員代表了該類對於一個實現細節的公開承諾。受保護的成員應該盡量少用。
有一條規則使得你無法降低一個方法的可訪問性。如果一個方法改寫了超類中的一個方法,那麼子類中該方法的存取層級低於超類中的存取層級時不允許的。這樣可以確保子類執行個體可以被用在任何可使用超類的執行個體的場合。如果你違反了這條規則,那麼當你試圖編譯該子類的時候,編譯器會產生一條錯誤資訊。這條規則的一種特殊情形是,如果一個類實現了一個介面,那麼介面中所有的方法在這個類中都必須被聲明為公有的。這是因為介面中所有方法都隱含著公有存取層級。
公有類應該儘可能地包含公有的域。如果一個域是非final的,或者是一個指向可變對象的final引用,那麼你一旦使它成為公有的,就放棄了對儲存在這個域中的值進行限制的能力;當這個域被修改的時候,你也失去了採取任何行動的能力。一個簡單的後果是,包含公有可變域的類不是安全執行緒地的。即使一個域是final的,並沒有指向任何一個可變對象,那麼,一旦你把這個域變成公有的,也就放棄了“切換到一個新的內部資料表示”的靈活性。
對於“公有類不應該包含公有域”這條規則也有一個例外,通過公有的靜態final域來暴露類的常量是允許的。按照慣例,這樣域的名字由大寫字母組成,單詞之間用底線隔開。很重要的一點是,這些域要麼包含原語類型的值,要麼包含指向非可變對象的引用。如果一個final域包含一個指向可變對象的引用,那麼它具有非final域的所有缺點。雖然引用本身不能被修改,但是它引用的對象可以被修改——這會導致災難性的後果。
注意,非零長度的數組總是可變的,所以,具有公有的靜態final數組域幾乎總是錯誤的。如果一個類包含這樣的一個域,客戶將能夠修改數組中的內容。這是安全性漏洞中的一個常見根源:
public static final Type[] VALUES = {...};private static final Type[] PRIVATE_VALUES ={...};public static final List VALUES =Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
另一種辦法是,如果你要求編譯時間的類型的安全性,並且願意損失一點效能的話,你可以把公有的數組替換成一個公有的辦法,它返回私人數組的一份拷貝:
private static final Type [] PRIVATE_VALUES = {...};public static final Type [] values(){return (Type[])PRIVATE_VALUES.clone();}
總之,你應該總會可能地降低可訪問性,再仔細設計了一個最小的公有API之後,應該防止把任何雜散的類、介面和成員變成API的一部分。除了公有靜態final域的特殊情形之外,公有類不應該包含公有域。並且確保公有靜態final域所引用對象是不可變的。