EffectiveJava--類和接口

[b]本章內容:[/b]
1. 使類和成員的可訪問性最小化
2. 在公有類中使用訪問方法而非公有域
3. 使可變性最小化
4. 複合優先於繼承
5. 要麼爲繼承而設計,並提供文檔說明,要麼就禁止繼承
6. 接口優於抽象類
7. 接口只用於定義類型
8. 類層次優於標籤類
9. 用函數對象表示策略
10. 優先考慮靜態成員類(嵌套類)

[b]1. 使類和成員的可訪問性最小化[/b]
設計良好的模塊會隱藏所有的實現細節,把它的API與它的實現清晰地隔離開來。然後,模塊之間只通過它們的API進行通信,一個模塊不需要知道其他模塊的內部工作情況。這個概念被稱爲信息隱藏或封裝,是軟件設計的基本原則之一。信息隱藏可以有效地解除組成系統的各模塊之間的耦合關係,使得這些模塊可以獨立的開發、測試、優化、使用、理解和修改。
Java程序設計語言提供了許多機制來協助信息隱藏。訪問控制機制決定了類、接口和成員的可訪問性,實體的可訪問性是由該類實體聲明所在的位置以及該實體聲明中所出現的訪問修飾符(private、protected、public)共同決定的。規則是儘可能地使每個類或者成員不被外界訪問,換句話說應該使用與你正在編寫的軟件功能一致、儘可能最小的訪問級別。

訪問級別:
私有的(private):只有在聲明該成員的頂層類內部纔可以訪問這個成員。
包級私有的:聲明該成員的包內部的任何類都可以訪問這個成員,缺省的訪問級別。
受保護的:聲明該成員的類的子類和該成員的包內部的類可以訪問這個成員。
公有的:在任何地方都可以訪問該成員。

(1)如果類或者接口能夠做成包級私有的,它就應該被做成包級私有。這樣在以後如果需要進行修改、替換、或者刪除無需擔心會影響到現有的客戶端程序。如果你把它做成公有的,你就有責任永遠支持它,以保持它們的兼容性。
(2)如果一個類只是在某一個類的內部被用到,就應該考慮使它成爲那個類的私有嵌套類。
(3)如果方法覆蓋了超類中的一個方法,子類中的訪問級別就不允許低於超類中的訪問級別。如果一個類實現了一個接口,那麼接口中所有的類方法在這個類中也都必須被聲明爲公有的,因爲接口中的所有方法都隱含着公有訪問級別。
(4)不能爲了測試,而將類、接口或者成員變成包的導出的API的一部分。可以讓測試作爲被測試的包的一部分來支行,從而能夠訪問它的包級私有的元素。
(5)實例域決不能是公有的。如果域是final的,或者是一個指向可變對象的final引用,那麼一旦使這個域成爲公有的,就放棄了對存儲在這個域中的值進行限制的能力。這意味着,你也放棄了強制這個域不可變的能力。同時,當這個域被修改的時候,你也失去了對它採取任何行動的能力。因此包含公有可變域的類並不是線程安全的。
(6)長度非零的數組總是可變的,所以類具有公有的靜態final數組域和返回這種域的方法都是錯誤的,如果類具有這樣的域或者訪問方法,客戶端將能夠修改數組中的內容。
public static final Thing[] VALUES={....};
許多IDE會產生返回指向私有數組域的引用的訪問方法,這樣就會產生上面的問題。修正這個問題有兩種方法:
一是使公有數組變成私有的,並增加一個公有的不可變列表。
二是使數組變成私有的,並添加一個公有方法,它返回私有數組的一個備份。

總而言之,你應該始終儘可能地降低可訪問性。在設計了一個最小的公有API之後,應該防止把任何散亂的類、接口和成員變成API的一部分。除了公有靜態final域的特殊情形之外,公有類都不應該包含公有域。並且要確保公有靜態final域所引用的對象都是不可變的。

[b]2. 在公有類中使用訪問方法而非公有域[/b]
如果類可以在它所在的包的外部進行訪問,就提供訪問方法,以保留將來改變該類的內部表示法的靈活性。
class Point{
private int a;
private int b;
public Point(int a, int b){
this.x = y;
this.y = y;
}
public int getA(){return a;}
public int getB(){return b;}
public void setA(int a){this.a = a;}
public void setB(int b){this.b = b;}
}
如果類是包級私有的,或者是私有的嵌套類,直接暴露它的數據域並沒有本質的錯誤。如有必要,不改變包之外的任何代碼而只改變內部數據表示法也是可以的。
讓公有類直接暴露域雖然從來都不是種好辦法,但是如果域是不可變的,這種做法的危害就比較小一些。

[b]3. 使可變性最小化[/b]
不可變類只是其實例不能被修改的類。每個實例中包含的所有信息都必須在創建該實例的時候就提供,並在對象的整個生命週期內固定不變。不可變類比可變類更加易於設計、實現和使用。它們不容易出錯,且更加安全。如String、基本類型的包裝類、BigInteger、BigDecimal等。
爲了使類成爲不可變,要遵循下面五條規則:
(1)不要提供任何會修改對象狀態的方法(mutator)
(2)保證類不會被擴展,防止子類假裝對象的狀態已經改變,從而破壞該類的不可變行爲。一般防止子類化就是使這個類成爲final的。
(3)使所有的域都是final的,或者讓類的所有構造器都變成私有的或者包級私有的,並添加公有的靜態工廠來代替公有的構造器
(4)使所有的域都成爲私有的,防止客戶端獲得訪問被域引用的可變對象的權限,並防止客戶端直接修改這些對象
(5)確保對於任何可變組件的互斥訪問

不可變對象本質上是線程安全的,它們不要求同步。不可變類應該充分利用這種優勢,鼓勵客戶端儘可能地重用現有的實例,要做到這一點,一個很簡便的辦法就是對於頻繁用到的值,爲它們提供公有的靜態final常量,
public static final String ERROR_TYPE = "xxx";
這種方法可以被進一步擴展,不可變的類可以提供一些靜態工廠,把頻繁被請求的實例緩存起來,從而當現有實例可以符合請求的時候,就不必創建新的實例。

不可變類真正唯一的缺點是,對於每個不同的值都需要一個單獨的對象。
如果能夠精確地預測出客戶端將要在不可變的類上執行哪些複雜的多階段操作, 可以提供一種包級私有的可變配套類,如String類的可變配套類StringBuilder。
堅決不要爲每個get方法編寫一個相應的set方法,除非有很多的理由要讓類成爲可變的類,否則就應該是不可變的。

[b]4. 複合優先於繼承[/b]
繼承是實現代碼重用的有力手段,但它並非永遠是完成這項工作的最佳工具。使用不當會導致軟件變得脆弱。在包的內部使用繼承是非常安全的,在這裏子類和超類的實現都處在同一個程序員的控制之下。對於專門爲了繼承而設計、並且具有很好的文檔說明的類來說,使用繼承也是非常安全的。
繼承打破了封裝性,換句話說,子類依賴於其超類中特定功能的實現細節,如果超類的實現隨着版本的不同而有所變化,子類可能會遭到破壞,即使子類的代碼完全沒有改變。因而子類必須要跟着其超類的更新而演變。

幸運的是,有一種辦法可以避免前面提到的所有問題。不用擴展現有的類,而是在新的類中增加一個私有域,它引用現有類的一個實例。這種設計被稱做“複合”,因爲現有的類變成了新類的一個組件。新類中的每個實例方法都可以調用被包含的現有類實例中對應的方法,並返回它的結果,這被稱爲轉發,新類中的方法被稱爲轉發方法。這樣得到的類將會非常穩固,它不依賴於現有類的實現細節,即使現有的類添加了新的方法,也不會影響新的類。新類被稱爲包裝類,這也正是Decorator模式,注意包裝類不適合用在回調框架中。實踐中包裝類不會對性能造成很大的影響。
只有當子類真正是超類的子類型時,才適合用繼承。即是說只有當兩者之間確實存在“is-a”關係的時候,一個類才能擴展另一個類。

[b]5. 要麼爲繼承而設計,並提供文檔說明,要麼就禁止繼承[/b]
首先,該類的文檔必須精確地描述覆蓋每個方法帶來的影響。換句話說,該類必須有文檔說明它可覆蓋的方法的自用性。即對於每個公有的或受保護的方法或者構造器,它的文檔必須指明該方法或者構造器調用了哪些可覆蓋的方法,是以什麼順序調用的,每個調用的結果又是如何影響後續的處理過程的(所謂可覆蓋的方法是指非final的,公有的或受保護的)。如果方法調用到了可覆蓋的方法,在它的文檔註釋的末尾應該包含關於這些調用的描述信息。
爲了能編寫出更加有效的子類,類必須通過某種形式提供適當的鉤子(hook),以便能夠進入到它的內部工作流程中,這種形式可以是精心選擇的受保護的方法,也可以是受保護的域。
構造器決不能調用可被覆蓋的方法,無論是直接調用還是間接調用。
如果類是爲了繼承而被設計的,實現Cloneable或Serializable接口都不是個好注意,因爲它們把一些實質性的負擔轉嫁到了擴展這個類的程序員的身上。當然可以採取一些特殊的手段使得子類實現這些接口(在謹慎的覆蓋clone和謹慎的實現Serialable接口兩節中有說到),即無論是clone還是readObject方法都不可以調用可覆蓋的方法,不管是直接還是間接的方式。如果你決定實現Serializable接口,並且該類有一個readResolve或者writeReplace方法,就必須使readResolve或者writeReplace成爲受保護的方法,而不是私有的方法,否則子類將會不聲不響的忽略掉這兩個方法。
對於那些並非爲了安全地進行子類化而設計和編寫文檔的類,要禁止子類化。禁止子類化一種方法就是把類聲明爲final的,另一種方法就是把所有的構造器都變成私有的,或者包級私有的,並增加一些公有的靜態工廠來替代構造器。

[b]6. 接口優於抽象類[/b]
這兩者機制之間最明顯的區別在於,抽象類允許包含某些方法的實現,但是接口則不允許,接口則不允許。另一個重要的區別在於定義的類必須成爲抽象類的一個子類,而java只允許單繼承,故抽象類作爲類型定義受到了極大的限制。
接口是定義mixin(增加功能混合到類型的主要功能上,如Comparable)的理想選擇。mixin是指這樣的類型:它除了實現它的“基本類型”之外,還可以實現這個mixin類型,以表明它提供了某些可供選擇的行爲。抽象類不能用於定義mixin。
通過對你導出的每個接口都提供一個抽象的骨架實現類,把接口和抽象類的優點結合起來。如AbstractCollection、AbstractSet等。骨架實現的美妙之處在於,它們爲抽象提供了實現上的幫助,但又不強加“抽象類被用作類型定義時”所特有的嚴格限制。

使用抽象類來定義允許多個實現的類型,與使用接口相比有一個明顯的優勢:抽象類的演變比接口的演變要容易得多。在抽象類中增加新方法並且實現後在所有的子類實現都將提供這個新的方法。對於接口這樣做是行不通的。
因此,設計公有的接口要非常謹慎,接口一旦被公開發行,並且已被廣泛實現,再想改變這個接口幾乎是不可能的。
總結,接口通常是定義允許多個實現的類型的最佳途徑。這條規則有個例外,即當演變的容易性比靈活性和功能更爲重要的時候,在這種情況下,應該使用抽象類來定義類型,但前提是必須理解並且可以接受這些侷限性。如果你導出了一個重要的接口,就應該堅決考慮同時提供骨架實現類。最後,應該儘可能謹慎地設計所有的公有接口,並通過編寫多個實現來對它們進行全面的測試。

[b]7. 接口只用於定義類型[/b]
當類實現接口時,接口就充當可以引用這個類的實例的類型(type)。因此類實現了接口,就表明客戶端可以對這個類的實例實施某些動作。爲了其他目的而使用接口是不恰當的。但除了常量接口(沒有包含任何方法,只包含靜態final域),使用這些常量的類實現這個接口,以避免用類名來修改常量名。
常量接口模式是對接口的不良使用。如果這些常量最好被看作枚舉類型的成員,就應該使用枚舉類型。否則,應該使用不可實例化的工具類來導出這些常量。工具類通常要求客戶端用類名來修飾這些常量名。也可以使用靜態導入,避免用類名修飾常量名
總結,接口應該只被用來定義類型,不應該被用來導出常量。

[b]8. 類層次優於標籤類[/b]
標籤類是指類中包含了表示實例風格的標籤域,它們中充斥着枚舉聲明,標籤域以及條件語句。標籤類過於冗長、容易出錯,並且效率低下。如下:
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape { RECTANGLE, CIRCLE };

// Tag field - the shape of this figure
final Shape shape;

// These fields are used only if shape is RECTANGLE
double length;
double width;

// This field is used only if shape is CIRCLE
double radius;

// Constructor for circle
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}

// Constructor for rectangle
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}

double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
替換標籤類更好的方法是子類型化,標籤類正是類層次的一種簡單的仿效。
爲了將標籤類轉變成類層次,首先要爲標籤類中的每個方法都定義一個包含抽象方法抽象類,這每個訪求的行爲都依賴於標籤值。接下來,爲每種原始標籤類都定義根類的具體子類。上面的類更改如下:
// Class hierarchy replacement for a tagged class
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;

Circle(double radius) { this.radius = radius; }

double area() { return Math.PI * (radius * radius); }
}
class Rectangle extends Figure {
final double length;
final double width;

Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double area() { return length * width; }
}
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}

這個類層次糾正了前面提到過的標籤類的所有缺點。它們可以用來反映類型之間本質上的層次關係,有助於增強靈活性,並進行更好的編譯時類型檢查。
標籤類很少有適用的時候。當你想要編寫一個包含顯示標籤域類時,應該考慮一下,這個標籤是否可以被取消,這個類是否可以用類層次來代替。當你遇到一個包含標籤域的現有類時,就要考慮將它重構到一個層次結構中去。

[b]9. 用函數對象表示策略[/b]
某些機制用於允許函數的調用者通過傳入第二個函數,來指定自己的行爲。這種機制被稱爲策略模式。
Java沒有提供函數指針,但是可以用對象引用實現同樣的功能。調用對象上的方法通常是執行該對象上某項操作,然而我們也可能定義這樣一種對象,它的方法執行其他對象(這些對象被顯式傳遞給這些方法)上的操作。如果一個類僅僅導出這樣的一個方法,它的實例實際上就等同於一個指向該方法的指針。這樣的實例被稱爲函數對象。如下:
class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
指向StringLengthComparator對象的引用可以被當做是一個指向該比較器的“函數指針”,可以在任意一對象字符串上被調用。換句話說,StringLengthComparator實例是用於字符串比較操作的具體策略。

作爲典型的具體策略類,StringLengthComparator類是無狀態的,他沒有域,所以這個類的所有實例在功能功能上都是等價的。因此,他作爲Singleton是非常合適的,可以節省不必要的對象創建開銷。
class StringLengthComparator2 {
private StringLengthComparator2(){}
public static final StringLengthComparator2 INSTANCE = new StringLengthComparator2();
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}

爲了客戶端能傳遞任何其他的比較策略,我們需要定義一個策略接口,如下:
public interface Comparable<T> {
public int compareTo(T t1, T t2);
}

class StringLengthComparator2 implements Comparable<String>{ ... }

具體的策略類往往使用匿名類聲明,如下:
Arrays.sort(stringArray,new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return s1.length() - s2.length();
}
});

但是,這樣每次執行調用的時候都創建一個新的實例。如果他被重複執行,考慮將實例對象存儲到一個私有的靜態final域裏,並重用他:
public class Host {
private static class StrLenCmp implements Comparator<String>, Serializable {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
}

函數指針的主要用途就是實現策略(Strategy)模式。爲了在Java中實現這種模式,要聲明一個接口來表示該策略,並且爲每個且體策略聲明一個實現了該接口的類。當一個具體策略只被使用一次時,通常使用匿名類來聲明和實例化這個具體策略類。當一個具體策略是設計用來重複使用的時候,它的類通常就要被實現爲私有的靜態成員類,並通過公有的靜態final域被導出,其類型爲該策略接口。

[b]10. 優先考慮靜態成員類(嵌套類)[/b]
嵌套類(nested class)是指被定義在另一個類的內部的類。嵌套類存在的目的應該只是爲他的外圍類(enclosing class)提供服務。
如果嵌套類將來可能會用於其他的某個環境中,他就應該是頂層類(top-level class)。 嵌套類有四種:靜態成員類(static member class)、非靜態成員類(nonstatic member class)、匿名類(anonymous class)和局部類(local class)。 除了第一種之外,其他三種都稱爲內部類(inner class)。

靜態成員類是最簡單的一種嵌套類,最好把它看作是普通的類,只是碰巧被聲明在另一個類的內部而已,它可以訪問外圍類的所有成員,包括那些聲明爲私有的成員。靜態成員類是外圍類的一個靜態成員,與其他的靜態成員一樣,也遵守同樣的可訪問性規則。靜態成員類的一種常見用法是作爲公有的輔助類,僅當與它的外部類一起使用時纔有意義。

從語法上講,靜態成員類和非靜態成員類之間的唯一區別是,靜態成員類的聲明中包含修飾符static。儘管他們的語法非常相似,但是兩種嵌套類有很大的不同。 非靜態成員類的每個實例都隱含着與外圍類的一個外圍實例(enclosing instance)相關聯。在非靜態成員類的實例方法內部,可以調用外圍實例上的方法,或者利用修飾過的this構造獲得外圍實例的引用。 如果嵌套類的實例可以在他外圍類的實力之外獨立存在,這個嵌套類就必須是靜態成員類:在沒有外圍實例的情況下,要想創建非靜態成員類的實例是不可能的。
當非靜態成員類的實例被創建的時候,他和外圍之間的關聯關係也隨之被建立起來;而且,這種關聯關係以後不能被修改。通常情況下,當在外圍類的某個實例方法的內部調用非靜態成員類的構造器時,這種關係被自動建立起來。 使用表達式enclosingInstance.new MemberClass(args)來手工建立這種關聯關係也是有可能的,但是很少使用。正如你所預料的那樣,這種關聯關係需要消耗費靜態成員類的實例空間,並且增加了構造的時間開銷。

非靜態成員類的一種常見用法是定義一個Adapter,它允許外部類的實例被看作是另一個不相關類的實例。

如果聲明成員類不要求訪問外圍實例,就要始終把static修飾符放在他的聲明中,使它成爲靜態成員類。如果省略了static修飾符,則每個實例都將包含一個額外的指向外圍對象的引用。保存這份引用要消耗時間和空間。並且會導致外圍實例在符合垃圾回收時卻仍然得以保留。如果在沒有外圍實例的情況下,也需要分配實例,就不能使用非靜態成員類,因爲非靜態成員類的實例必須要有一個外圍實例。
私有靜態成員類的一種常見用法是用來代表外圍類所代表的對象的組件。

匿名類不同於Java程序設計語言中的其他任何語法單元,正如你所想象的,匿名類沒有名字。它不是處置類的一個成員,它並不與其他的成員一起被聲明,而是在使用的同時被聲明和實例化。匿名類可以出現在代碼中任何允許存在表達式的地方。當且僅當匿名類出現在非靜態的環境中,它纔有外圍實例。但是即使它們出現在靜態的環境中,也不可能擁有任何靜態成員。
匿名類除了在它們被聲明的時候之外,是無法將它們實例化的。你不能執行instanceof測試,或者做任何需要命名類的其他事情。你無法聲明一個匿名類來實現多個接口,或者擴展一個類。匿名類的客戶端無法調用任何成員,除了從它的超類型中繼承得到之外。由於匿名類出現在表達式中,它們必須保持簡短(大約10行或者更少),否則會影響程序的可讀性。
匿名類的一種常見用法是動態的創建函數對象(如上一知識點)。 另一種常見用法是創建過程對象(如Runnable、Thread或TimerTask實例)。 第三種常見的用法是在靜態工廠方法的內部。

局部類是四種嵌套類中用得最少的類。在任何可以聲明局部變量的地方,都可以聲明局部類。與匿名類一樣,它們必須非常簡短,以便不會影響可讀性。

簡而言之,如果一個嵌套類需要在單個方法之外仍然是可見的,或者他太長了,不適合方法內部,就應該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用,就要把成員類做成非靜態的;否則就做成靜態的。假設這個嵌套類屬於一個方法的內部,如果你只需要在一個地方創建實例,並且已經有了一個預置的類型可以說明這個類的特徵,就要把他做成匿名類;否則,就做成局部類。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章