《Effective Java》學習筆記(三)——類和介面
資訊隱藏或封裝,可以有效地解除組成系統的各模組之間的耦合關係,使得這些模組可以獨立地開發、測試、優化、使用、理解和修改。
Java程式設計語言提供了許多機制來協助資訊隱藏。訪問控制機制決定了類、介面和成員的可訪問性。
第一規則:儘可能地使每個類或者成員不被外界訪問。
對於頂層的(非巢狀的)類和介面,只有兩種可能的訪問級別:包級私有的(package-private)和公有的(public)。如果一個包級私有的的頂層類或者介面只是在某一個類的內部被用到,就應該考慮使它成為唯一使用它的那個類的私有巢狀類。
對於成員(域、方法、巢狀類和巢狀介面)有四種可能的訪問級別,下面按照可訪問性的遞增順序羅列出來:
- 私有的(private)——只有在宣告該成員的頂層類內部才可以訪問這個成員;
- 包級私有的(package-private)——宣告該成員的包內部的任何類都可以訪問這個成員。從技術上講,它被稱為“預設(default)訪問級別”,如果沒有為成員指定訪問修飾符,就採用這個訪問級別;
- 受保護的(protected)——宣告該成員的類的子類可以訪問這個成員,並且宣告該成員的包內部的任何類也可以訪問這個成員;
- 公有的(public)——在任何地方都可以訪問該成員。
對於公有類的成員,當訪問級別從包級私有變成保護級別時,會大大增強可訪問性。
如果方法覆蓋了超類中的一個方法,子類中的訪問級別就不允許低於超類中的訪問級別。這樣可以確保任何可使用超類的例項的地方也都可以使用子類的例項。一種特殊情形:如果一個類實現了一個介面,那麼介面中所有的類方法在這個類中也都必須被宣告為公有的。
例項域決不能是公有的。包含公有可變域的類並不是執行緒安全的。
靜態域也同樣決不能是公有的。只有一種例外情況:假設常量構成了類提供的整個抽象中的一部分,可以通過公有的靜態final域來暴露這些常量。
長度非零的陣列總是可變的,所以,類具有公有的靜態final陣列域,或者返回這種域的訪問方法,這幾乎總是錯誤的。
在公有類中使用訪問方法而非公有域
如果類可以在它所在的包的外部進行訪問,就提供訪問方法,以保留將來改變該類的內部表示法的靈活性。
但是,如果類是包級私有的,或者是私有的巢狀類,直接暴露它的資料域並沒有本質的錯誤。
讓共有類直接暴露域從來都不是種好辦法,但是如果域是不可變的,這種做法的危害就比較小一些。
使可變性最小化
不可變類只是其例項不能被修改的類。每個例項中包含的所有信息都必須在建立該例項的時候就提供,並在物件的整個生命週期內固定不變。
為了使類成為不可變,要遵循下面五條規則:
- 不要提供任何會修改物件狀態的方法(也成為mutator)。
- 保證類不會被擴充套件。為了防止子類化,一般做法是使這個類成為final的。
- 使所有的域都是final的。
- 使所有的域都成為私有的。
- 確保對於任何可變元件的互斥訪問。
不可變物件本質上是執行緒安全的,它們不要求同步。不可變物件可以被自由地共享。
不可變的類可以提供一些靜態工廠,它們把頻繁被請求的例項快取起來,從而當現有例項可以符合請求的時候,就不必建立新的例項,從而降低記憶體佔用和垃圾回收的成本。
“不可變物件可以被自由地共享”導致的結果是,永遠也不需要進行保護性拷貝。實際上根本無需做任何拷貝,因為這些拷貝始終等於原始的物件。
不僅可以共享不可變物件,甚至可以共享它們的內部資訊。
不可變物件為其他物件提供了大量的構件(building blocks),無論是可變的還是不可變的物件。
不可變類真正唯一的缺點是,對於每個不同的值都需要一個單獨的物件。
如果你執行一個多步驟的操作,並且每個步驟都會產生一個新的物件,除了最後的結果之外其他的物件最終都會被丟棄,此時效能問題就會顯露出來。如果能夠精確地預測出客戶端將要在不可變的類上執行哪些複雜的多階段操作,這種包級私有的可變配套類的方法就可以工作的很好;如果無法預測,最好的辦法就是提供一個公有的可變配套了。在Java平臺類庫中,這種方法的主要例子是String類,它的可變配套類是StringBuilder。
讓不可變的類變成final的另一種方法是,讓類的所有構造器都變成私有的或者包級私有的,並新增公有的靜態工廠來代替公有的構造器。
堅決不要為每個get方法編寫一個相應的set方法,除非有很好的理由要讓類成為可變的類,否則就應該是不可變的。
如果類不能被做成不可變的,仍然應該儘可能地限制它的可變性。除非有令人信服的理由要使域變成非final的,否則要使每個域都是final的。
複合優先於繼承
在包的內部使用繼承是非常安全的,在那裡,子類和超類的實現都處在同一個程式設計師的控制之下。
與方法呼叫不同的是,繼承打破了封裝性。
只有當子類和超類之間確實存在子型別關係時,使用繼承才是恰當的。即便如此,如果子類和超類處在不同的包中,並且超類並不是為了繼承而設計的,那麼繼承將會導致脆弱性。為了避免這種脆弱性,可以用複合和轉發機制來代替繼承。
不用擴充套件現有的類,而是在新的類中增加一個私有域,它引用現有類的一個例項。這種設計被稱作“複合(composition)”,因為現有的類變成了新類的一個元件。新類中的每個例項方法都可以呼叫被包含的現有類例項中對應的方法,並返回它的結果,這被稱為轉發(forwarding),新類中的方法被稱為轉發方法。——這也正是Decorator模式。
要麼為繼承而設計,並提供文件說明,要麼就禁止繼承
對於專門為了繼承而設計並且具有良好文件說明的類而言,該類的文件必須精確地描述覆蓋每個方法所帶來的影響。該類必須有文件說明它可覆蓋的自用性。
關於程式文件有句格言:好的API文件應該描述一個給定的方法做了一個什麼工作,而不是描述它是如何做到的。
為了繼承而進行的設計不僅僅涉及自用模式的文件設計。為了使程式設計師能夠編寫出更加有效的子類,而無需承受不必要的痛苦,類必須通過某種形式提供適當的鉤子(hook),以便能夠進入到它的內部工作流程中,這種形式可以是精心選擇的受保護的方法,也可以是受保護的域,後者比較少見。
對於為了繼承而設計的類,唯一的測試方法就是編寫子類。如果遺漏了關鍵的受保護成員,嘗試編寫子類就會使遺漏所帶來的痛苦變得更加明顯。
為了允許繼承,類還必須遵守其他一些約束:構造器決不能呼叫可被覆蓋的方法,無論是直接呼叫還是間接呼叫。
如果你決定在一個為了繼承而設計的類中實現Serializable,並且該類有一個readResolve或者writeReplace方法,就必須是readResolve或者writeReplace成為受保護的方法,而不是私有的方法。
對於那些並非為了安全地進行子類化而設計和編寫文件的類,要禁止子類化。
介面優於抽象類
Java程式設計語言提供了兩種機制,可以用來定義允許多個實現的型別:介面和抽象類。這兩種機制之間最明顯的區別在於,抽象類允許包含某些方法的實現,但是介面則不允許。因為Java只允許單繼承,所以抽象類作為型別定義收到了極大的限制。
現有的類可以很容易被更新,以實現新的介面。
介面是定義mixin(混合型別)的理想選擇。
介面允許我們構造非層次結構的型別框架。
通過包裝類模式,介面使得安全地增強類的功能成為可能。
通過對你匯出的每個重要介面都提供一個抽象的骨架實現類(AbstractInterface),把介面和抽象類的優點結合起來。介面的作用仍然是定義型別,但是骨架實現類接管了所有與介面實現相關的工作。
設計公有的介面要非常謹慎。介面一旦被公開發行,並且已被廣泛實現,再想改變這個介面幾乎是不可能的。
介面只用於定義型別
當類實現介面時,介面就充當可以引用這個類的例項的型別。因此類實現了介面,就表明客戶端可以對這個類的例項實施某些動作。為了任何其他目的而定義介面是不恰當的。
常量介面:這種介面沒有包含任何方法,只包含靜態的final域,每個域匯出一個常量。使用這些常量的類實現這個介面,以避免用類名來修飾常量名。常量介面模式是對介面的不良使用。
如果要匯出常量:如果這些常量與某個現有的類庫或者介面緊密相關,就應該把這些常量新增到這個類或者介面中;如果這些常量是最好被看作列舉型別的成員,就應該用列舉型別來匯出這些常量;否則,應該使用不可例項化的工具類來匯出這些常量。
工具類通常要求客戶端要用類名來修飾這些常量名,如果大量利用工具類匯出的常量,可以通過利用靜態匯入機制,避免用類名來修飾常量名。
類層次優於標籤類
有時候,可能會遇到帶有兩種甚至更多種風格的例項的類,幷包含表示例項風格的標籤域。但是標籤類過於冗長、容易出錯,並且效率低下。
Java提供了其他更好的方法來定義能表示多種風格物件的單個數據型別:子型別化。標籤類是類層次的一種簡單的仿效。
為了將標籤類轉變成類層次,首先要為標籤類中的每個方法都定義一個包含抽象方法的抽象類,這每個方法的行為都依賴於標籤值。接下來,為每種原始標籤類都定義根類的具體子類,在每個子類中都包含特定於該型別的資料域,同時在每個子類中還包括針對根類中每個抽象方法的相應實現。
類層次的另一種好處在於,它們可以用來反映型別之間本質上的層次關係,有助於增強靈活性,並進行更好的編譯時型別檢查。
用函式物件表示策略
Java沒有提供函式指標,但是可以用物件引用實現同樣的功能。呼叫物件上的方法通常是執行該物件上的某些操作。然而,我們也可能定義這樣一種物件,它的方法執行其他物件(這些物件被顯式傳遞給這些方法)上的操作。如果一個類僅僅匯出這樣的一個方法,它的例項實際上就等同於一個指向該方法的指標。這樣的例項被稱為函式物件。
函式指標的主要用途就是實現策略模式。為了在Java中實現這種模式,要宣告一個介面來表示該策略,並且為每個具體策略宣告一個實現了該介面的類。當一個具體策略只被使用一次時,通常使用匿名類來宣告和例項化這個具體策略類。當一個具體策略是設計用來重複使用的時候,它的類通常就要被實現為私有的靜態成員類,並通過公有的靜態final域被匯出,其型別為該策略介面。
優先考慮靜態成員類
巢狀類是指被定義在另一個類內部的類。巢狀類存在的目的應該只是為它的外圍類提供服務。如果巢狀類將來可能會用於其他的某個環境中,它就應該是頂層類。巢狀類有四種:靜態成員類、非靜態成員類、匿名類和區域性類。除了第一種之外,其他三種都被稱為內部類。
靜態成員類是最簡單的一種巢狀類。最好把它看作是普通的類,只是碰巧被宣告在另一個類的內部而已,它可以訪問外圍類的所有成員,包括哪些宣告為私有的成員。靜態成員類是外圍類的一個靜態成員,與其他的靜態成員一樣,也遵守同樣的可訪問性規則。如果它被宣告為私有的,它就只能外圍類的內部才可以被訪問。
靜態成員類的一種常見用法是作為公有的輔助類,僅當與它的外部類一起使用時才有意義。
從語法上講,靜態成員類和非靜態成員類之間唯一的區別是,靜態成員類的宣告中包含修飾符static。非靜態成員類的每個例項都隱含著與外圍類的一個外圍例項相關聯。在非靜態成員類的例項方法內部,可以呼叫外圍例項上的方法,或者利用修飾過的this構造獲得外圍例項的引用。