1. 程式人生 > >Effective Java 3rd 條目15 最小化類和成員的可訪問性

Effective Java 3rd 條目15 最小化類和成員的可訪問性

區分良好設計與不良設計的元件的最重要的因素是,這個元件對其他元件隱藏它的內部資料和其他實現細節的程度。一個設計良好的元件隱藏了它的所有實現細節,乾淨地把它的API從它的實現中分離。然後元件僅僅可以通過它們的API通訊,而且不知道它們彼此的內部運作。這個概念,叫資訊隱藏(information hiding)或者封裝(encapsulation),是軟體設計的一個基礎原則[Parnas72]。

資訊隱藏在許多方面是重要的,大多數起源於這個事實:它解耦(decouples)了組成系統的元件,讓它們可以開發、測試、優化、使用、理解和隔離修改。這加快了系統開發,因為元件可以並行地開發。它減輕了維護的負擔,因為元件可以更快地理解、除錯或者不要擔心危害其他元件地替代。雖然資訊隱藏和它本身並不能帶來好的效能,但是它使得效能調優有效率:一旦一個系統完成了,效能分析決定了哪個元件造成了效能問題(條目67),這些元件可以沒有影響其他元件正確的情形下優化。資訊隱藏增加了軟體複用,因為沒有緊耦合的元件,除了它們被開發的情形下,常常證明在其他情形下是有用的。最後,資訊隱藏減少了構建大系統的危險性,因為單個元件可以證明是成功的,即使系統並沒有。

Java有協助資訊隱藏的許多工具。訪問控制(access control)機制[JLS, 6.6] 規定了類、介面和成員的訪問性(accessibility)。實體的訪問性由下面兩者決定:它的宣告位置和在宣告時呈現哪個(如果有)訪問識別符號(private、protected和public)。這些修飾符正常使用對於資訊隱藏是必要的。

經驗法則是簡單的:使得每個類或者成員儘量不可訪問。換句話說,使用與你編寫軟體的正常功能一致的儘可能低的訪問等級。

對於頂層的(非巢狀的)類和介面,有兩個可能的訪問等級:包私有的(package-private)公開的(public)。如果你聲明瞭一個公開修飾符的頂層類或者介面,它是公開的;否則,它是包私有的。如果頂層的類或者介面是包私有的,它應該是這樣。通過使得它是包私有的,你可以使得它是實現的一部分,而不是可以匯出的API,而且你可以修改它、替代它,或者在下一部的釋出中移除它,而不用擔心已經存在的客戶端。如果你使得它是公開的,那麼你有義務為了維護相容性而永久支援它。

如果是包私有的頂層類或者介面僅僅由一個類使用,考慮把頂層類變成是使用它的唯一類的私有靜態巢狀類(條目24)。這減少了它的包裡面的所有類對使用它的那個類的訪問。但是相對於包私有的頂層類,減少一個不必要的公開類的訪問重要得多:公開類是包的API,而包私有的頂層類已經是它實現的一部分。

對於成員(域、方法、巢狀類和巢狀介面),有四種可能的訪問等級,以增加訪問性順序列出如下:

  • 私有的(private) – 成員僅僅可以從在宣告它的頂層類裡訪問

  • 包私有的(package-private) – 成員可以從在宣告它的包裡面的任何類訪問。技術上被認為是預設的訪問。如果沒有指定訪問修飾符(除了介面成員,它預設是公開的),這是你得到的訪問等級。

  • 受保護的(protected) – 成員可以從宣告它的類的子類訪問(受到一些限制[JLS, 6.6.2]),而且可以從宣告它的包裡面的任何類訪問。

  • 公開的(public) – 成員可以從任何地方訪問。

在小心地設計你的類的公開API之後,你第一反應應該是使得所有其他的成員是私有的。只有在同一個包裡面的另外一個類真正需要訪問一個成員,你才應該移除私有修飾符而使得這個成員是包私有的。如果你發現自己經常這麼做,你應該重新檢測系統的設計,看看從另一個類解耦比較好的類是否需要另外一個分解。也就是說,私有的和包私有的成員是類實現的一部分,而通常不會影響它的匯出API。然而,如果類實現了系列化(Serializable)(條目86和87),這些域可能洩漏到匯出的API。

對於公開類的成員,當訪問等級從包私有到受保護時,訪問性的急劇增加將會發生。訪問的物件是類匯出API的一部分,而且必須永遠支援。而且,一個匯出類的受保護的成員代表著實現細節的一個公開承諾(條目19)。受保護成員的需求是相等少見的。

有個重要的規則是,限制你的能力來減少方法的訪問性。如果一個方法覆寫了一個超類,在子類中它不能有比超類中更嚴格的訪問等級[JLS, 8.4.8.3]。這是必要的,保證子類例項在超類例項可用的地方都可以使用(里氏代換原則(Liskov substitution principle),參考條目15)。如果你違反了這個規則,那麼當你試著編譯子類時編譯器將會產生一個錯誤資訊。這個規則的特例是,如果類實現了一個介面,所有介面中的類方法在類中必須宣告為公開的。

為了方便測試你的程式碼,你可能傾向於讓一個類、介面或者成員有比原本需要的更多訪問性。這在某種程度是可以的。為了測試讓一個公開類的私有成員成為包私有的,這是可接受的,但是再提高可訪問性是不可接受的。換句話說,為了方便測試,讓一個類、介面或者成員成為一個包匯出API一部分,這是不可接受的。幸運的是,我們也沒必要,因為測試可以作為需要測試包的一部分而執行,所以可以訪問它的包私有元素。

公開類的例項方法應該很少是公開的(條目16)。如果一個例項域是非final的或者是一個可變物件的引用,讓它成為公開的,那麼你放棄了限制儲存在域中的值的能力。這意味著,這你放棄了實現這個域成為不變類的能力。而且,當類改變的時候,你放棄了採取任何行動的能力,所以有公開可變域的類通常不是執行緒安全的。即使一個域是final而且引用了一個可變物件,讓它成為公開的,你就放棄了切換到一個新內部資料呈現(這個域不存在)的靈活性。

同樣的建議可以應用到靜態域,但是有一個例外。你可以通過公開靜態final域來暴露常量,假設這些常量是組成這個類提供的抽象的不可分割的一部分。按照慣例,這些域有大寫字元的名字,單詞用下劃線分割(條目68)。這些域要麼包含原始值,要麼包含對不可變物件的引用(條目17)。一個包含可變物件引用的域,有非final域的所有缺點。雖然這個引用不可能改變,但是被引用的物件可以修改–有災難性的結果。

注意到,非零長度的佇列總是可變的,所以一個類有一個公開靜態final佇列域或者有返回這種域的訪問子(accessor)是錯誤的。如果一個類有這樣的域或者訪問子,那麼客戶端可以修改佇列的內容。下面是一個安全漏洞的常見來源:

// 潛在的安全漏洞!
public static final Thing[] VALUES = { ... };

注意這個事實,一些IDE產生返回一個私有佇列域的引用,恰恰導致了這個問題。有兩個方法解決這個問題。你可以讓公開佇列成為私有的而且新增一個公開不可變列表:

private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final List<Thing> VALUES = 
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

或者,你可以讓佇列成為私有,而且新增一個返回私有佇列拷貝的公開方法:

private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final Thing[] values() { 
    return PRIVATE_VALUES.clone(); 
}

為了從這些備選方案選擇,想想客戶端可能對結果做什麼。哪個返回型別更方便?哪個將有更好的效能?

在Java9中,有兩個額外的隱含訪問等級引入為模組系統(module system)的一部分。一個模組是一組包,就像包是一組類。一個模組通過在它的模組宣告(module declaration)(通常包含在一個叫做module-info.java的原始檔)中匯出宣告(export declaration),可以顯式地匯出它的有些包。模組裡未匯出包的公開和受保護成員在模組外面是不可訪問的;在模組裡面,訪問性是不受匯出宣告影響的。使用模組系統讓你可以在模組裡面的包之間分享類,而沒有讓它們對於整個世界可見。在未匯出包裡面的公開類的公開和受保護成員,導致了兩個隱含的訪問等級,它們可類比於正常公開和受保護等級。這種分享的需求是相當少見的,而且常常可能通過重新安排包裡面的類來解決。

不像四個主要的訪問等級,這兩個基於模組的等級是建議性質的。如果你在你的應用的類途徑而不是它的模組途徑上放置一個模組的JAR檔案,那麼這個模組的包恢復到它們的非模組化行為:包的公開類的所有公開和受保護成員有它們的正常訪問性,而不管包是否有這個模組匯出[Reinhold, 1.2]。新引入的訪問等級被嚴格執行的地方,是JDK本身:Java庫中的非匯出包在它們的模組外是真正不可訪問的。

對於一個典型的Java程式設計師,不只是由模組提供的訪問保護是功能有限的,而且在本質上是建議性質的;為了利用它,你必須把你的包分組到模組中,使得它們的所有依賴在模組宣告是明顯的,重新安排你的原始碼樹,而且採取特別的行動:在你的模組裡面容納對非模組化包的任何引用[Reinhold, 3]。現在就說模組在JDK本身之外是否能廣泛使用還為時過早。同時,除非你有一個迫切的需求,似乎最好是避免它們。

總之,你應該儘可能減少(明智地)對程式元素的訪問性。在仔細設計一個最小限度公開API後,你應該阻止散落的任何類、介面或者成員成為API的一部分。除了作為常量的公開靜態final域,公開類不應該有公開域。確保由公開靜態final域引用的類是不可變的。