有些類不希望被實例化,對它們實例化也沒有意義。
因為聲明的構(gòu)造函數(shù)是私有的,所以它在類的外部不可訪問。假設(shè)構(gòu)造函數(shù)不會被類自身從內(nèi)部調(diào)用,即能保證類永遠(yuǎn)不會被實例化。 例:
class F {
private F() {
...
}
...
}
重用同一對象比每次都創(chuàng)建功能等同的新對象通常更適合。重用方式既快速也更加時尚。
終結(jié)程序(finalizer)的行為是不可預(yù)測的,而且是危險的,通常也不必要。不要把終結(jié)程序作為C++析構(gòu)函數(shù)(destructor)的類似物。
終結(jié)程序無法保證能別及時的執(zhí)行,這意味著不能用終結(jié)程序來處理時間關(guān)鍵(time-critical)性的操作。例如,依賴終結(jié)程序去關(guān)閉打開的文件是一個嚴(yán)重的錯誤,因為打開的文件描述符是一種有限資源,而JVM不會及時地安排終結(jié)程序執(zhí)行,如果多個文件處于打開狀態(tài),程序有可能會因為無法再打開文件而執(zhí)行失敗。
JLS不僅不保證終結(jié)程序的及時執(zhí)行,它甚至不保證終結(jié)程序會獲得執(zhí)行。永遠(yuǎn)不要依賴終結(jié)程序去更新關(guān)鍵的持續(xù)狀態(tài)(persistent state)。例如,依靠終結(jié)程序釋放一個共享資源如數(shù)據(jù)庫上的持續(xù)鎖,將是導(dǎo)致所有分布系統(tǒng)跨掉的絕佳方法。
非可變類就是類的實例不能被修改的類。如String。
為了使類成為非可變的,要遵循下面5條原則
下面是一個稍微復(fù)雜的例子:
public final class Complex {
private final float re;
private final float im;
public Complex(float re, float im) {
this.re = re;
this.im = im;
}
public float realPart() { return re; }
public float imaginaryPart() { return im; }
public Complex add(Complex c) {
return new Complex(re+c.re,im+c.im);
}
...
public boolean equals(Object o) {
if(o==this)
return true;
if(!(o instanceOf Complex))
return false;
Complex c = (Complex) 0;
return(Float.floatToIntBits(re) == Float.floatToIntBits(c.re) ) &&
(Float.floatToIntBits(im) == Float.floatToIntBits(c.im));
}
public int hashCode() {
int result = 17 + Float.floatToIntBits(re);
result = 37*result + Float.floatToIntBits(im);
return result;
}
public String toString() {
return "("+re+" + "+im+"i)";
}
}
這個類表示復(fù)數(shù),注意到算術(shù)操作創(chuàng)建和返回一個新的復(fù)數(shù)實例,而不是修改了這個實例。
繼承是實現(xiàn)代碼重用的有力途徑,但它不總是完成這項工作的最后的工具。與方法調(diào)用不同,繼承打破了封裝性。子類的特有的功能,依賴于它的超類的實現(xiàn)細(xì)節(jié)。超類的實現(xiàn)會隨著版本而改變,如果出現(xiàn)這樣的情況,即使不觸動子類的代碼,它也會被破壞。
不去擴(kuò)展現(xiàn)有的類,而是給類增加一個引用現(xiàn)有類實例的新的私有域,這種設(shè)計方法被成為復(fù)合(composition),因為現(xiàn)有的類成為了新的類的一部分。
繼承只有當(dāng)子類確實是超類的“子類型”(subtype)時,才是適合的。換句話說,對兩個類A和B,如果“B是A”的關(guān)系存在,那么B應(yīng)該擴(kuò)展A。在把B擴(kuò)展A時,問這樣一個問題:“任何的B都是A嗎?”,如果答案是否定的,那么通常應(yīng)該把A作為B的一個私有實例,然后暴露更小、更簡單的API:A不是B的基本部分,只是它的實現(xiàn)細(xì)節(jié)。
類必須提供文檔準(zhǔn)確地描述重載任一方法的效果。類必須說明它的可重載方法的自用性(self-use):對每個公共的或受保護(hù)的方法或構(gòu)造函數(shù),它的文檔信息都必須要表明它在調(diào)用哪一個可重載的方法、以什么順序調(diào)用及每一個調(diào)用的結(jié)果如何影響后面的處理。
為了允許程序員有效地進(jìn)行子類化處理而不必承受不必要的痛苦,類必須用認(rèn)真選擇的受保護(hù)方法提供它內(nèi)部實現(xiàn)的鉤子(hook)。
構(gòu)造函數(shù)一定不能調(diào)用可重載的方法,無法直接地還是間接地。超類構(gòu)造函數(shù)會在子類構(gòu)造函數(shù)之前運(yùn)行,所以子類中的重載方法會在子類構(gòu)造函數(shù)運(yùn)行之前被調(diào)用。如果重載方法依賴于由子類構(gòu)造函數(shù)執(zhí)行的初始化,那么該方法將不會按期望的方式執(zhí)行。
clone和readObject方法都不能調(diào)用可重載的方法,無論是直接的還是間接的。如果確定要用來繼承的類實現(xiàn)Serializable,并且類中有一個readResolve或writeReplace方法,那么必須使readResolve或writeReplace方法成為受保護(hù)的而不是私有的。一旦這些方法是私有的,它們就將被子類悄然忽略。
對那些不是專門設(shè)計用于安全地實現(xiàn)子類化并具有文檔說明的類,禁止子類化。
禁止子類化的方法有兩種
Java語言為定義允許有多種實現(xiàn)的類型提供了兩種機(jī)制:接口和抽象類。
現(xiàn)有的類可以很容易的被更新以實現(xiàn)新的接口。所有需要做的工作是增加還不存在的方法并在類的聲明中增加一個implement語句。
接口是定義混合類型(mixins)的理想選擇。mixin是這樣的類型:除了它的“基本類型(primary type)”外,類還可以實現(xiàn)額外的類型,以表明它提供了某些可選的功能。
接口允許非層次類型框架的構(gòu)造。對于組織某些事物,類型層次是極好的選擇,但有些事物不能清楚地組織成嚴(yán)格的層次。
接口通過使用封裝類方式,能夠獲得安全、強(qiáng)大的功能。如果使用抽象類定義類型,那么程序員在增加功能是,除了使用繼承外別無選擇,而且得到的類與封裝類相比功能更差、更脆弱。
盡管接口不允許方法實現(xiàn),但使用接口定義類型并不妨礙給程序員提供實現(xiàn)上的幫助??梢酝ㄟ^抽象的構(gòu)架實現(xiàn)類與希望輸出的所有重要的接口配合,從而將接口與抽象類的優(yōu)點(diǎn)組合在一起。
使用抽象類定義具有多個實現(xiàn)的類型與使用接口相比有一個明顯優(yōu)勢:演進(jìn)抽象類比演進(jìn)接口更容易。如果在以后的版本重,需要給抽象類增加新方法,那么總可以增加一個包含正確的缺省實現(xiàn)的具體方法。此時所有現(xiàn)存的該抽象類的實現(xiàn)都會具有這個新的方法。
嵌套類(nested class)是一種定義在其他類內(nèi)部的類。嵌套類應(yīng)該僅僅為包容它的類而存在。
+ - 有四種類型嵌套類:
靜態(tài)成員類是最簡單的嵌套類。它最好被看做是普通的類碰巧被聲明在其他的類的內(nèi)部。它對所有封閉類中的成員有訪問權(quán)限,甚至那些私有成員。靜態(tài)成員類的一種通常用法是作為公共的輔助類,僅當(dāng)和它的外部類協(xié)作時才有意義。
如果聲明了一個不要求訪問封閉實例的成員類,切記要在它的聲明里使用static修飾符,把成員類變?yōu)殪o態(tài)的。 私有靜態(tài)成員類的一般用法是用來表示它們的封閉類對象的組件。
每一個非靜態(tài)的成員類與它包含類的封閉實例(enclosing instance)隱式地關(guān)聯(lián)。如果嵌套類的實例可以在它的封閉類實例之外單獨(dú)存在,那么嵌套類不能成為非靜態(tài)成員類:創(chuàng)建沒有封閉實例的非靜態(tài)成員類實例是不可能的。非靜態(tài)成員實例與它的封閉實例之間的關(guān)聯(lián)在前者被創(chuàng)建時即建立,在此之后它不能被修改。
過長的匿名類會傷害程序的可讀性。
//Typical use of an anonymous class
Arrays.sort(args, new Comparator() {
public int compare(Object o1,Object o2) {
return ((String)o1).length() - ((String)o2).length();
}
}
每個類實例本質(zhì)上是唯一的。
不關(guān)心類是否提供了“邏輯意義的等同”(logical equality)測試。例如java.util.Random本來可以重載equals方法,用以檢查兩個Random實例是否會產(chǎn)生相同的隨機(jī)數(shù)序列,但設(shè)計者不認(rèn)為客戶會需要或想要這個功能。這種情況下,使用從Object繼承的equals實現(xiàn)就夠了。
超類已經(jīng)重載了equals,而從超類繼承的行為適合該類。
類是私有的或包內(nèi)私有(package-private)的,而且可以確定它的equals方法永遠(yuǎn)不會被調(diào)用。
在重載equal方法時要重載hashCode方法。
不要使自己聰明過頭。把任何的同義形式考慮在比較的范圍內(nèi)一般是糟糕的想法,例如File類不應(yīng)該與指向同一文件的符號鏈接進(jìn)行比較,實際上File類也沒有這樣做。
不要設(shè)計依賴于不可靠資源的equals方法。
不要將equals聲明中的Object替換為其他類型。程序員編寫出形如下面所示的equals方法并不少見,它會讓人摸不清頭腦:所設(shè)計方法為什么不能正確工作:
public boolean equals(Myclass o) {
...
}
問題出在這個方法沒有重載(override)參數(shù)為Object類型的Object.equals方法。而是過載(overload)了它。這在正常的equals方法中,又提供了一個“強(qiáng)類型”的equals方法。
一定要在每一個重載了equals的類中重載hashCode方法。不這樣做會違背Object.hashCode的一般約定,并導(dǎo)致你的類與所有基于散列的集合一起作用時不能正常工作,這些集合包括HashMap、HashSet和Hashtable。
不重載hashCode方法違背了java.lang.Object的規(guī)范:相等的對象必須有相等的散列碼。兩個截然不同的實例根據(jù)類的equals方法也許邏輯上是等同的,但對于Object類的hashCode方法,它們就是兩個對象,僅此而已。因而對象的hashCode方法返回兩個看上去是隨機(jī)的數(shù)值,而不是約定中要求的相等的值。
好的hash函數(shù)傾向于為不相等的對象生成不相等的hash碼。理想的情況下,hash函數(shù)應(yīng)該把所有不相等的實例的合理集合均一地分布到所有可能的hash值上去。達(dá)到理想狀態(tài)很難,但是下面有一種相對合適的方法
i.如果域是boolean型,計算(f?0:1)。
ii.如果域是byte型、char型、short型或int型,計算(int)f。
iii.如果域是long型,計算(int)(f^(f>>>32))。
iv.如果域是float型,計算Float.floattoIntBits(f)。
v.如果域是double型,計算Double.doubleToLongBits(f),然后如2.a.iii所示,對long型結(jié)果進(jìn)一步處理。
vi.如果域是對象引用,而且這個類的equals方法又遞歸地調(diào)用了equals方法對域進(jìn)行比較,那么對這個域遞歸地調(diào)用hashCode方法。如果需要一種更復(fù)雜的比較方式,那么先為這個域計算出“范式表示”,然后在該“范式表示”上調(diào)用hashCode方法。如果域為null,則返回0。
vii.如果域是數(shù)組,則把每個元素作為分離的域?qū)Υ?。即遞歸地使用這些規(guī)則,為每個“主要元素”計算hash碼。然后用2.b所示方法復(fù)合這些值。
如果方法沒有對參數(shù)做檢查,會出現(xiàn)幾種情形。
對那些不被方法使用但會被保存以供使用的參數(shù),檢查有效性尤為重要。
一種重要的例外是有效性檢查開銷高,或者不切實際,而且這種有效性檢查在計算的過程中會被隱式地處理的情形。
總的來說,每次在設(shè)計方法或設(shè)計構(gòu)造函數(shù)時,要考慮它們的參數(shù)有什么限制。要在文檔中注釋出這些限制,并在方法體的開頭通過顯示的檢查,對它們進(jìn)行強(qiáng)化。養(yǎng)成這樣的習(xí)慣是重要的,有效性檢查所需要的不多的工作會從它的好處中得到補(bǔ)償。
必須在客戶會使用一切手段破壞類的約束的前提下,保護(hù)性地設(shè)計程序。
為了保護(hù)Period實例的內(nèi)部細(xì)節(jié)免于這種攻擊,對構(gòu)造函數(shù)的每一個可變參數(shù)使用保護(hù)性拷貝是必要的。
保護(hù)性拷貝要在參數(shù)的有效性檢查的前面,并且有效性檢測要在副本而不是初值上執(zhí)行。
幾乎每次返回null而不是返回0長度數(shù)組的方法時,都需要這種多余地處理。返回null是易出錯的,因為寫代碼的程序員可能會忘記設(shè)計處理返回null的特殊情形的代碼。
如果存在合適的接口類型,那么參數(shù)、返回值、變量和域應(yīng)該用接口類型聲明。
應(yīng)該養(yǎng)成下面這樣的程序習(xí)慣:
//Good - uses interface as type
List subscribers = new Vector();
而不要這樣做:
//Bad - uses class as type!
Vector subscribers = new Vector();
如果養(yǎng)成了使用接口作為類型的習(xí)慣,程序就會有更好的擴(kuò)展性。當(dāng)希望轉(zhuǎn)換實現(xiàn)時,需要做的全部工作就是改變構(gòu)造函數(shù)中類的名字。