第4章 類和接口
使類和成員的可訪問性最小化
模塊之間只通過它們的API進行通信, 一個模塊不需要知道其他模塊的工作情況范嘱,這稱之為信息隱藏(information hiding)或封裝(encapsualtion)实苞,是軟件設計的基本原則之一。
靈活使用成員(域、方法奈应、嵌套類和嵌套接口)的四種訪問級別。
- 私有的(private)
- 包訪問的(package-private)
- 受保護的(protected)
- 公有的(public)
其中私有和包訪問則是類的實現(xiàn)中的一部分即不會影響它的導出的API购披。
受保護的成員的應該盡量少用杖挣。
包含公有可變域的類并不是線程安全的。這一點主要說明的是我們應該多使用不可變的域刚陡,即多使用final來達到我們的目的惩妇。PS:final并不是萬能的解決方案,即當final指向一個可變對象的引用筐乳,同樣也會帶來問題歌殃。
長度非零的數(shù)組總是可變的,所以蝙云,類具有共有的靜態(tài)final數(shù)組域氓皱,或者返回這種域的訪問方法,這幾乎總是錯誤的。如下:
public static final Thing[] values = {...};
有如下的兩種修正方法:
- 使公有數(shù)組變成私有的波材,并增加一個公有的不可變列表:
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableLIst(Arrays.asList(PRIVATE_VALUES));
- 使數(shù)組變成私有的股淡,并添加一個公有方法,返回私有數(shù)組的一個備份:
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values(){
return PRIVATE_VALUES.clone();
}
總結:應該始終盡可能地降低可訪問性廷区。在仔細地設計了一個最小的公有API之后唯灵,應該防止把任何散亂的類、接口和成員變成API的一部分隙轻。除了公有靜態(tài)final域的特殊情形之外埠帕,公有類都不應該包含公有域。并且要確保公有靜態(tài)final域所引用的對象都是不可變的玖绿。
在公有類中使用訪問方法而非公有域
如果類可以在它所在的包的外部進行訪問敛瓷,就提供訪問方法,以保留將來改變該類的內(nèi)部表示法的靈活性镰矿。
當域不可變的時候琐驴,在讀取域時,可強加約束條件秤标。
公有類永遠都不應該暴露可變的域绝淡。雖然還是有問題,但是讓公有類暴露不可變的域其危害比較小苍姜。但是牢酵,有時候會需要用包級私有的或者私有的嵌套類來暴露域,無論這個類是不變的還是不可變的衙猪。
是可變性最小化
Java平臺類庫中包含的不可變類馍乙,有String,基本類型的包裝類垫释,BigInteger和BigDecimal丝格。不可變的類比可變類更加易于設計、實現(xiàn)和使用棵譬,不容易出錯且更加安全显蝌。
為了使類成為不可變,要遵循下面五條原則:
- 不要提高任何會修改對象狀態(tài)的方法订咸。
- 保證類不會擴展
- 使所有的于都是final的曼尊。
- 使所有的域成為私有的。
- 確保對于任何可變組件的互斥訪問脏嚷。
采用函數(shù)的做法骆撇,即在方法中返回的是一個新的實例,而不是修改這個實例父叙。
不僅可以共享不可變對象神郊,甚至也可以共享它們的內(nèi)部信息肴裙。如BigInteger類。
不可變對象為其他對象提供了大量的構建涌乳。
不可變類真正唯一的缺點是践宴,對于每個不同的值都需要一個單獨的對象。
為了確保不可變性爷怀,類絕對不允許自身被子類話,除了“使類成為final的”這種方法之外带欢,還有另外一種更加靈活的辦法可以做到這一點:讓類的所有構造器都變成私有的或者包級私有的运授,并添加公有的靜態(tài)工廠(static factory)來代替公有的構造器。
復合優(yōu)先于繼承
繼承打破了封裝性乔煞。換句話說吁朦,子類依賴于其超類中特定功能的實現(xiàn)細節(jié)。超類的實現(xiàn)有可能會隨著發(fā)行版本的不同有所變化渡贾,如果真的發(fā)生了變化逗宜,子類可能遭到破壞,即使它的代碼完全沒有改變空骚。
繼承機制會把超類API中的所有缺陷傳播到子類中纺讲,而復合則允許設計新的API來隱藏這些缺陷。
要么為繼承而設計囤屹,并提供文檔說明熬甚,要么就禁止繼承
為了設計一個類的文檔,以便它能夠被安全地子類化 肋坚,你必須描述清楚那些有可能未定義的實現(xiàn)細節(jié)乡括。
為了允許繼承,類還必須遵守其他一些約束智厌。構造器不能調(diào)用可被覆蓋的方法诲泌。
你可以機械地消除類中可覆蓋方法的自用特性,而不改變它的行為铣鹏。將每個可覆蓋方法的代碼體移到一個私有的"輔助方法(helper method)"中敷扫,并且讓每個可覆蓋的方法調(diào)用它的私有輔助方法。然后吝沫,用“直接調(diào)用可覆蓋方法的私有輔助方法”來代替“可覆蓋方法的每個自用調(diào)用”呻澜。
接口優(yōu)于抽象類
Java只允許單集成,所以抽象類作為類型定義受到了極大的限制惨险。而相對地羹幸,接口則有以下好處:
- 現(xiàn)有的類可以很容易被更新,以實現(xiàn)新的接口辫愉。(抽象類則需要修改類層次栅受,單繼承也會給我們帶來不小的困擾。)
- 接口是定義mixin(混合類型)的理想選擇。
- 接口允許我們構造非層次結構的類型框架屏镊。
Sometime, 我們需要對接口來提供一個抽象的骨架實現(xiàn)類(skeletal implementation), 把接口和抽象類的優(yōu)點結合起來依疼。骨架實現(xiàn)通常會以 AbstractInterface的形式出現(xiàn),如 Collections Framework中的AbstractCollection而芥、AbstractSet律罢、AbstractList和AbstractMap。
抽象類的演變比接口的演變要容易得多棍丐。在抽象類中增加新的方法误辑,則該抽象類的所有現(xiàn)有實現(xiàn)都將提供這個新的方法。對于接口歌逢,這樣做是行不通的巾钉。
總結: 接口通常是定義允許多個實現(xiàn)的類型的最佳途徑。一個例外就是秘案,當演變的容易性比靈活性和功能更為重要的時候砰苍,這種情況下,應該使用抽象類阱高,但前提是必須理解并且可以接受這些局限性赚导。如果導出了一個重要的接口,就應該堅決考慮同時提供骨架實現(xiàn)類讨惩。最后辟癌,應該盡可能謹慎地設計所有的公有接口,并通過編寫多個實現(xiàn)來對它們進行全面的測試荐捻。
接口只用于定義類型
接口應該只被用來定義類型黍少,不應該被用來導出常量。常量接口模式是對接口的不良使用处面。應該使用不可實例化的工具類(utility class)來導出這些常量厂置。
如果大量利用工具類導出的常量,可以通過利用靜態(tài)導入(static import)機制魂角,避免用類名來修飾常量名昵济,(靜態(tài)導入機制是在Java1.5中才引入的)。
類層次優(yōu)于標簽類
標簽類是指帶有兩種設置更多風格的實例的類野揪,并包含實例風格的標簽(tag)域访忿。
它有著有多缺點,其中充斥著樣板代碼斯稳,包括枚舉聲明海铆、標簽域及條件語句。一句話:標簽類過于冗長挣惰,容易出錯卧斟,并且效率低下殴边。
解決方法:使用子類型化(subtyping)為每種原始標簽類定義根類的具體子類。
類層次的另一種好處:可以反映類型之間本質(zhì)上的層次關系珍语,有助于增強靈活性锤岸,并進行更好的編譯時類型檢查。
用函數(shù)對象表示策略
常用的比較器函數(shù)就代表一種為元素排序的策略板乙。
總結:函數(shù)指針的主要用途就是實現(xiàn)策略(Strategy)模式是偷。為了在Java實現(xiàn)這種模式,要聲明一個接口來表示該策略募逞,并且為每個具體策略聲明一個實現(xiàn)了該接口的類晓猛。當一個具體策略只被使用一次時,通常使用匿名類來聲明和實例化這個具體策略類凡辱。當一個具體策略是設計用來重復使用過的時候,它的類通常就要被實現(xiàn)為私有的靜態(tài)成員類栗恩,并通過公有的靜態(tài)final域被導出透乾,其類型為該策略接口。
優(yōu)先考慮靜態(tài)成員類
嵌套類(nested class)是指被定義在另一個類的內(nèi)部的類磕秤。其有四種:靜態(tài)成員類(static member class)乳乌、非靜態(tài)成員類(nonstatic member class)、匿名類(anonymous class)市咆、和局部類(local class)汉操,后三種都被成為內(nèi)部類(inner class)。
靜態(tài)成員類可以訪問外圍類的所有成員蒙兰,包括哪些聲明為私有的成員磷瘤。靜態(tài)成員類是外圍類的一個靜態(tài)成員,與其他的靜態(tài)成員一樣搜变,也遵守同樣的可訪問性規(guī)則采缚。如果被聲明為私有的,它則只能在外圍類的內(nèi)部才可以被訪問挠他。
常見的地方:Map的集合視圖(collection view)keySet扳抽、entrySet和Values;Set和List集合接口中的迭代器(iterator)殖侵。
非靜態(tài)成員類的每個實例都隱含著與外部類的一個外圍實例(enclosing instance)相關聯(lián)贸呢。這種關聯(lián)關系需要消耗非靜態(tài)成員類實例的空間,并且增加了構造的時間開銷拢军。
當且僅當匿名類出現(xiàn)在非靜態(tài)的環(huán)境中楞陷,它才有外圍實例。當出現(xiàn)在靜態(tài)的環(huán)境中朴沿,則不會用于任何靜態(tài)成員猜谚。常用用法是動態(tài)地創(chuàng)建行數(shù)對象(function object);創(chuàng)建過程對象(process object)如败砂,Runnable, Thread 或 TimerTask實例;在靜態(tài)工廠方法的內(nèi)部魏铅。
局部類有名字昌犹,可以被重復地使用。與匿名類一樣览芳,只有在非靜態(tài)環(huán)境中定義的時候斜姥,才有外圍實例,它們也不能包含靜態(tài)成員沧竟。
** 總結:** 四種嵌套類各有用途铸敏。若果一個嵌套類需要在單個方法之外仍然是可見的,或者太長了悟泵,不適合在方法內(nèi)部杈笔,就應該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用糕非,就要把成員類做成非靜態(tài)的蒙具;否則,就做成靜態(tài)的朽肥。假設這個嵌套類屬于一個方法的內(nèi)部禁筏,如果你只需要在一個地方創(chuàng)建實例,并且已經(jīng)有了一個預置的類型可以說明這個類的特征衡招,就要把它做成匿名類篱昔;否則就做成局部類。