第4章 類和接口
使類和成員的可訪問性最小化
模塊之間只通過它們的API進行通信, 一個模塊不需要知道其他模塊的工作情況,這稱之為信息隱藏(information hiding)或封裝(encapsualtion),是軟件設計的基本原則之一。
靈活使用成員(域、方法、嵌套類和嵌套接口)的四種訪問級別。
- 私有的(private)
- 包訪問的(package-private)
- 受保護的(protected)
- 公有的(public)
其中私有和包訪問則是類的實現中的一部分即不會影響它的導出的API。
受保護的成員的應該盡量少用。
包含公有可變域的類并不是線程安全的。這一點主要說明的是我們應該多使用不可變的域,即多使用final來達到我們的目的。PS:final并不是萬能的解決方案,即當final指向一個可變對象的引用,同樣也會帶來問題。
長度非零的數組總是可變的,所以,類具有共有的靜態final數組域,或者返回這種域的訪問方法,這幾乎總是錯誤的。如下:
public static final Thing[] values = {...};
有如下的兩種修正方法:
- 使公有數組變成私有的,并增加一個公有的不可變列表:
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();
}
總結:應該始終盡可能地降低可訪問性。在仔細地設計了一個最小的公有API之后,應該防止把任何散亂的類、接口和成員變成API的一部分。除了公有靜態final域的特殊情形之外,公有類都不應該包含公有域。并且要確保公有靜態final域所引用的對象都是不可變的。
在公有類中使用訪問方法而非公有域
如果類可以在它所在的包的外部進行訪問,就提供訪問方法,以保留將來改變該類的內部表示法的靈活性。
當域不可變的時候,在讀取域時,可強加約束條件。
公有類永遠都不應該暴露可變的域。雖然還是有問題,但是讓公有類暴露不可變的域其危害比較小。但是,有時候會需要用包級私有的或者私有的嵌套類來暴露域,無論這個類是不變的還是不可變的。
是可變性最小化
Java平臺類庫中包含的不可變類,有String,基本類型的包裝類,BigInteger和BigDecimal。不可變的類比可變類更加易于設計、實現和使用,不容易出錯且更加安全。
為了使類成為不可變,要遵循下面五條原則:
- 不要提高任何會修改對象狀態的方法。
- 保證類不會擴展
- 使所有的于都是final的。
- 使所有的域成為私有的。
- 確保對于任何可變組件的互斥訪問。
采用函數的做法,即在方法中返回的是一個新的實例,而不是修改這個實例。
不僅可以共享不可變對象,甚至也可以共享它們的內部信息。如BigInteger類。
不可變對象為其他對象提供了大量的構建。
不可變類真正唯一的缺點是,對于每個不同的值都需要一個單獨的對象。
為了確保不可變性,類絕對不允許自身被子類話,除了“使類成為final的”這種方法之外,還有另外一種更加靈活的辦法可以做到這一點:讓類的所有構造器都變成私有的或者包級私有的,并添加公有的靜態工廠(static factory)來代替公有的構造器。
復合優先于繼承
繼承打破了封裝性。換句話說,子類依賴于其超類中特定功能的實現細節。超類的實現有可能會隨著發行版本的不同有所變化,如果真的發生了變化,子類可能遭到破壞,即使它的代碼完全沒有改變。
繼承機制會把超類API中的所有缺陷傳播到子類中,而復合則允許設計新的API來隱藏這些缺陷。
要么為繼承而設計,并提供文檔說明,要么就禁止繼承
為了設計一個類的文檔,以便它能夠被安全地子類化 ,你必須描述清楚那些有可能未定義的實現細節。
為了允許繼承,類還必須遵守其他一些約束。構造器不能調用可被覆蓋的方法。
你可以機械地消除類中可覆蓋方法的自用特性,而不改變它的行為。將每個可覆蓋方法的代碼體移到一個私有的"輔助方法(helper method)"中,并且讓每個可覆蓋的方法調用它的私有輔助方法。然后,用“直接調用可覆蓋方法的私有輔助方法”來代替“可覆蓋方法的每個自用調用”。
接口優于抽象類
Java只允許單集成,所以抽象類作為類型定義受到了極大的限制。而相對地,接口則有以下好處:
- 現有的類可以很容易被更新,以實現新的接口。(抽象類則需要修改類層次,單繼承也會給我們帶來不小的困擾。)
- 接口是定義mixin(混合類型)的理想選擇。
- 接口允許我們構造非層次結構的類型框架。
Sometime, 我們需要對接口來提供一個抽象的骨架實現類(skeletal implementation), 把接口和抽象類的優點結合起來。骨架實現通常會以 AbstractInterface的形式出現,如 Collections Framework中的AbstractCollection、AbstractSet、AbstractList和AbstractMap。
抽象類的演變比接口的演變要容易得多。在抽象類中增加新的方法,則該抽象類的所有現有實現都將提供這個新的方法。對于接口,這樣做是行不通的。
總結: 接口通常是定義允許多個實現的類型的最佳途徑。一個例外就是,當演變的容易性比靈活性和功能更為重要的時候,這種情況下,應該使用抽象類,但前提是必須理解并且可以接受這些局限性。如果導出了一個重要的接口,就應該堅決考慮同時提供骨架實現類。最后,應該盡可能謹慎地設計所有的公有接口,并通過編寫多個實現來對它們進行全面的測試。
接口只用于定義類型
接口應該只被用來定義類型,不應該被用來導出常量。常量接口模式是對接口的不良使用。應該使用不可實例化的工具類(utility class)來導出這些常量。
如果大量利用工具類導出的常量,可以通過利用靜態導入(static import)機制,避免用類名來修飾常量名,(靜態導入機制是在Java1.5中才引入的)。
類層次優于標簽類
標簽類是指帶有兩種設置更多風格的實例的類,并包含實例風格的標簽(tag)域。
它有著有多缺點,其中充斥著樣板代碼,包括枚舉聲明、標簽域及條件語句。一句話:標簽類過于冗長,容易出錯,并且效率低下。
解決方法:使用子類型化(subtyping)為每種原始標簽類定義根類的具體子類。
類層次的另一種好處:可以反映類型之間本質上的層次關系,有助于增強靈活性,并進行更好的編譯時類型檢查。
用函數對象表示策略
常用的比較器函數就代表一種為元素排序的策略。
總結:函數指針的主要用途就是實現策略(Strategy)模式。為了在Java實現這種模式,要聲明一個接口來表示該策略,并且為每個具體策略聲明一個實現了該接口的類。當一個具體策略只被使用一次時,通常使用匿名類來聲明和實例化這個具體策略類。當一個具體策略是設計用來重復使用過的時候,它的類通常就要被實現為私有的靜態成員類,并通過公有的靜態final域被導出,其類型為該策略接口。
優先考慮靜態成員類
嵌套類(nested class)是指被定義在另一個類的內部的類。其有四種:靜態成員類(static member class)、非靜態成員類(nonstatic member class)、匿名類(anonymous class)、和局部類(local class),后三種都被成為內部類(inner class)。
靜態成員類可以訪問外圍類的所有成員,包括哪些聲明為私有的成員。靜態成員類是外圍類的一個靜態成員,與其他的靜態成員一樣,也遵守同樣的可訪問性規則。如果被聲明為私有的,它則只能在外圍類的內部才可以被訪問。
常見的地方:Map的集合視圖(collection view)keySet、entrySet和Values;Set和List集合接口中的迭代器(iterator)。
非靜態成員類的每個實例都隱含著與外部類的一個外圍實例(enclosing instance)相關聯。這種關聯關系需要消耗非靜態成員類實例的空間,并且增加了構造的時間開銷。
當且僅當匿名類出現在非靜態的環境中,它才有外圍實例。當出現在靜態的環境中,則不會用于任何靜態成員。常用用法是動態地創建行數對象(function object);創建過程對象(process object)如,Runnable, Thread 或 TimerTask實例;在靜態工廠方法的內部。
局部類有名字,可以被重復地使用。與匿名類一樣,只有在非靜態環境中定義的時候,才有外圍實例,它們也不能包含靜態成員。
** 總結:** 四種嵌套類各有用途。若果一個嵌套類需要在單個方法之外仍然是可見的,或者太長了,不適合在方法內部,就應該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用,就要把成員類做成非靜態的;否則,就做成靜態的。假設這個嵌套類屬于一個方法的內部,如果你只需要在一個地方創建實例,并且已經有了一個預置的類型可以說明這個類的特征,就要把它做成匿名類;否則就做成局部類。