- 創(chuàng)建及銷毀對象
- + - 考慮用靜態(tài)工廠方法替代構(gòu)造函數(shù)
靜態(tài)工廠方法的優(yōu)勢
靜態(tài)工廠方法的一個優(yōu)勢是,它們具有自己的名字。構(gòu)造函數(shù)的參數(shù)自身無法描述被返回的對象,而選用名字合適的靜態(tài)工廠方法可以使類的使用更加容易,產(chǎn)生的客戶代碼更容易閱讀。
靜態(tài)工廠方法的第二個優(yōu)勢是它們不需要在每次調(diào)用時都去創(chuàng)建一個新的對象。這使得非可變類可以使用預(yù)先構(gòu)造的實例,或者在構(gòu)造階段先緩存這些實例,然后重復(fù)使用它們,從而避免創(chuàng)建不必要的重復(fù)對象。
- 靜態(tài)工廠方法為重復(fù)調(diào)用而返回同一對象的能力,可以用來控制某一時刻實例的存在情況。
- 有兩個理由使靜態(tài)方法可以做到這一點。
- 首先它能夠使類保證實例是singleton。
- 其次,它能夠使非可變類保證沒有兩個相等的實例同時存在。
靜態(tài)工廠的第三個優(yōu)勢是它們可以返回到返回類型的任何子類型(subtype)對象。這使用戶在選擇返回對象的類時具有很大的靈活性。
靜態(tài)工廠的缺陷
靜態(tài)工廠的主要缺陷是類在沒有公共或受保護的構(gòu)造函數(shù)時不能被子類化。例如,不可能子類化Collections Framework中的任何集合類。
靜態(tài)工廠方法的另一個缺陷是名字之間不容易區(qū)分。
總地來說,如果你權(quán)衡過了這兩種方法,同時沒有其他因素影響你的選擇取向,那么最好還是簡單地使用構(gòu)造函數(shù),因為這符合規(guī)范。
- + - 使用私有構(gòu)造函數(shù)強化singleton屬性
- singleton類就是一種只能被實例化一次的簡單類。
- + - 實現(xiàn)singlton有兩種途徑,這兩種途徑都以保持構(gòu)造函數(shù)私有及提供公共的靜態(tài)成員允許客戶訪問它的唯一實例為基礎(chǔ)。
-
一種實現(xiàn)方法是,公共的靜態(tài)成員是一個final域: //singleton with final field public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } ... } 私有構(gòu)造函數(shù)只被調(diào)用一次,以初始化公共的靜態(tài)final域Elvis.INSTANCE。公共的或受保護的構(gòu)造函數(shù)的缺乏,保證了一個“唯一的elvis”的世界:一旦Elvis類被初始化,僅有一個Elvis實例存在。
第一種方式的突出優(yōu)點是類的成員的聲明使類清楚地表明它是singleton:公共的靜態(tài)域是final,所以這個域?qū)⒖偸前嗤膶ο笠谩?
第二種方法中,提供了公共的靜態(tài)工廠方法,取代了公共的靜態(tài)final域: //singleton with static factory public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public static Elvis getInstance() { return INSTANCE; } } 對所有的靜態(tài)方法Elvis.getInstance的調(diào)用,都返回同一個對象的引用,沒有其他的Elvis實例被創(chuàng)建。
第二種方式的突出優(yōu)點在于它給使用者提供了靈活性,當(dāng)你決定把這個類改變?yōu)榉莝ingleton的時候,無需修改API。
總而言之,如果你確信該類將永遠是singleton的,那么應(yīng)該使用第一種方法。反之,第二種方式是更好的選擇。
- + - 用私有構(gòu)造函數(shù)強化不可實例化能力
-
有些類不希望被實例化,對它們實例化也沒有意義。
試圖通過將類抽象化來強化類的不可實例化能力是行不通的。這是因為類可以被子類化,而子類可以被實例化。這種做法還會誤導(dǎo)用戶,以為這種類的目的是為了實現(xiàn)繼承的。
有一種簡單的方法可以解決這一問題。由于缺省的構(gòu)造函數(shù)僅在類不包含顯示的構(gòu)造函數(shù)時才會生成,我們可以在類中包含顯式的私有類型構(gòu)造函數(shù)來實現(xiàn)類的不可實例化特性。
因為聲明的構(gòu)造函數(shù)是私有的,所以它在類的外部不可訪問。假設(shè)構(gòu)造函數(shù)不會被類自身從內(nèi)部調(diào)用,即能保證類永遠不會被實例化。 例: class F { private F() { ... } ... }
- + - 避免創(chuàng)建重復(fù)對象
-
重用同一對象比每次都創(chuàng)建功能等同的新對象通常更適合。重用方式既快速也更加時尚。
下面的語句是個極端的例子,千萬不要這樣做: String s = new String("Silly"); //never do this 該語句每次執(zhí)行時都創(chuàng)建一個新的String實例,然而這些對象的創(chuàng)建是不必要的。
一個簡單的版本如下: String s = "No longer silly";
除了對非可變對象重用,我們也可以重用可變(mutable)對象,只要知道它不會發(fā)生變化。下面是一個例子,不要使用這種做法: public class Person { private final Date birthDate; public Person(Date birthDate) { this.birthDate = birthDate; }
//Don‘t do this! public boolean isBabyBoomer() { Calendar gmtCal = Calender.getInstance(Timezone.getTimezone("GMT")); gmtCal.set(1946, Calender.JANUARY, 1, 0, 0, 0); Date boomStart = gmtCal.getTime(); gmtCal.set(1965, Calender.JANUARY, 1, 0, 0, 0); Date boomEnd = gmtCal.getTime(); return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) <0; } } 在每次被調(diào)用時,方法isBabyBoomer都不必要地創(chuàng)建了一個新的Calendar、TimeZone和兩個Date實例。
下面的版本通過使用靜態(tài)方法的初始化方法避免了這樣的低效做法。 public class Person { private final Date birthDate; public Person(Date birthDate) { this.birthDate = birthDate; } private static final Date BOOM_START; private static final Date BOOM_END;
static { Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); gmtCal.set(1946, Calender.JANUARY, 1, 0, 0, 0); Date BOOM_START = gmtCal.getTime(); gmtCal.set(1965, Calender.JANUARY, 1, 0, 0, 0); Date BOOM_END = gmtCal.getTime(); }
public boolean isBabyBoomer() { return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) <0; } }
- + - 消除對過期對象的引用
-
例: //Can you spot the "memory leak"? public class Stack { private Object[] elements; private int size = 0; public Stack(int initialCapacity) { this.elements = new Object[initialCapacity]; }
public void push(Object e) { ensureCapacity(); elements[size++] = e; }
public Object pop() { if(size =- 0) throw new EmptyStackException(); return elements[--size]; }
private void ensureCapacity() { if(elements.length == size) { Object[] OldElements = elements; elements = new Object[2*elements.lenght+1]; System.arraycopy(OldElements,0,elements,0,size); } } }
這個程序沒有明顯的錯誤,但是卻隱藏著一個問題,不嚴(yán)格地講,程序有一個“內(nèi)存漏洞”。如果棧先增長再收縮,那么被棧彈出的對象即使在程序不再引用它們時,也不會被垃圾回收單元回收。這是由于棧維護著對這些對象的過期引用(obsolete reference)。所謂過期引用,是指永遠不會被解除引用的引用。
解決這種問題的方法很簡單:一旦它們過期,清除掉對它們的引用。 pop正確的版本是: public Object pop() { if(size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; return result; }
清除掉過期引用的額外好處是,如果它們隨后被錯誤地解除引用,程序?qū)捎贜ullPointerException異常退出,而不是繼續(xù)錯誤地運行下去。
- + - 避免使用終結(jié)程序
-
終結(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í)行。永遠不要依賴終結(jié)程序去更新關(guān)鍵的持續(xù)狀態(tài)(persistent state)。例如,依靠終結(jié)程序釋放一個共享資源如數(shù)據(jù)庫上的持續(xù)鎖,將是導(dǎo)致所有分布系統(tǒng)跨掉的絕佳方法。
- 類和接口
- + - 最小化類和成員的可訪問能力
- 區(qū)分設(shè)計良好與設(shè)計拙劣的模塊的唯一也是最重要的元素是模塊向外部模塊隱藏內(nèi)部數(shù)據(jù)和其他實現(xiàn)細節(jié)的程度。
- 經(jīng)驗規(guī)則指出應(yīng)該使每個類或成員盡可能地不被外部訪問。換句話說,在設(shè)計程序時,應(yīng)該根據(jù)軟件功能,使用允許的最低的訪問等級。
- 有一條規(guī)則限制了我們給方法降低可訪問性的能力:如果方法要重載超類中的方法,那么不允許子類中該方法的訪問等級低于超類中的訪問等級。
- + - 傾向于非可變性
-
非可變類就是類的實例不能被修改的類。如String。
非可變類有很多好的存在理由
- 非可變類更容易設(shè)計、實現(xiàn)和使用
- 不易犯錯誤
- 更安全
為了使類成為非可變的,要遵循下面5條原則
不要提供任何修改對象的方法。
保證沒有可以被重載的方法。這防止了粗心的或惡意的子類危害對象的不可變行為。防止方法被重載的一般方式是是類成為final的。
使所有的域成為final。
使所有的域都是私有的。
保證對任何可變組件互斥訪問。
下面是一個稍微復(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ù)實例,而不是修改了這個實例。
非可變對象本質(zhì)上是線程安全的,不需要同步機制。這是獲得線程安全最簡單的途徑。線程不會看到其他線程對非可變對象施加的影響,因而,非可變對象可以被自由地共享。
非可變對象可以提供一個靜態(tài)工廠,將經(jīng)常被請求的實例緩存起來,避免在被請求的實例存在時,重復(fù)地創(chuàng)建新的實例。BigInteger和Boolean類都具有這種靜態(tài)工廠方法。
不僅可以共享非可變對象,還可以共享它們的內(nèi)部信息。
非可變對象為其他對象——無論是可變還是非可變的,創(chuàng)建了大量的構(gòu)造塊。
非可變對象的真正也是唯一的缺點是對每個不同的值要求一個單獨的對象。
- + - 組合優(yōu)于繼承
-
繼承是實現(xiàn)代碼重用的有力途徑,但它不總是完成這項工作的最后的工具。與方法調(diào)用不同,繼承打破了封裝性。子類的特有的功能,依賴于它的超類的實現(xiàn)細節(jié)。超類的實現(xiàn)會隨著版本而改變,如果出現(xiàn)這樣的情況,即使不觸動子類的代碼,它也會被破壞。
不去擴展現(xiàn)有的類,而是給類增加一個引用現(xiàn)有類實例的新的私有域,這種設(shè)計方法被成為復(fù)合(composition),因為現(xiàn)有的類成為了新的類的一部分。
繼承只有當(dāng)子類確實是超類的“子類型”(subtype)時,才是適合的。換句話說,對兩個類A和B,如果“B是A”的關(guān)系存在,那么B應(yīng)該擴展A。在把B擴展A時,問這樣一個問題:“任何的B都是A嗎?”,如果答案是否定的,那么通常應(yīng)該把A作為B的一個私有實例,然后暴露更小、更簡單的API:A不是B的基本部分,只是它的實現(xiàn)細節(jié)。
- + - 設(shè)計和文檔化繼承
-
類必須提供文檔準(zhǔn)確地描述重載任一方法的效果。類必須說明它的可重載方法的自用性(self-use):對每個公共的或受保護的方法或構(gòu)造函數(shù),它的文檔信息都必須要表明它在調(diào)用哪一個可重載的方法、以什么順序調(diào)用及每一個調(diào)用的結(jié)果如何影響后面的處理。
為了允許程序員有效地進行子類化處理而不必承受不必要的痛苦,類必須用認(rèn)真選擇的受保護方法提供它內(nèi)部實現(xiàn)的鉤子(hook)。
構(gòu)造函數(shù)一定不能調(diào)用可重載的方法,無法直接地還是間接地。超類構(gòu)造函數(shù)會在子類構(gòu)造函數(shù)之前運行,所以子類中的重載方法會在子類構(gòu)造函數(shù)運行之前被調(diào)用。如果重載方法依賴于由子類構(gòu)造函數(shù)執(zhí)行的初始化,那么該方法將不會按期望的方式執(zhí)行。
例: public class Super { //Broken - constructor invokes overridable method public Super(){ m(); } public void m() { } } 下面的子類重載了m,而m被超類唯一的構(gòu)造函數(shù)錯誤地調(diào)用了: final class sub extends Super { private final Date date; //Blank final, set by constructor Sub() { date = new Date(); } //Overrides Super.m, invoked by the constructor Super() public void m() { System.out.println(date); } public static void main(String[] args) { Sub s = new Sub(); s.m(); } } 它第一次打印出的是null。這是因為方法m被構(gòu)造函數(shù)Super()在構(gòu)造函數(shù)Sub()初始化date域之前被調(diào)用。
clone和readObject方法都不能調(diào)用可重載的方法,無論是直接的還是間接的。如果確定要用來繼承的類實現(xiàn)Serializable,并且類中有一個readResolve或writeReplace方法,那么必須使readResolve或writeReplace方法成為受保護的而不是私有的。一旦這些方法是私有的,它們就將被子類悄然忽略。
對那些不是專門設(shè)計用于安全地實現(xiàn)子類化并具有文檔說明的類,禁止子類化。
禁止子類化的方法有兩種
- 最容易的方法是將類聲明為final的。
- 另一種是使所有的構(gòu)造函數(shù)私有或者成為包內(nèi)私有,并用公共的靜態(tài)工廠取代構(gòu)造函數(shù)。
- + - 接口優(yōu)于抽象類
-
Java語言為定義允許有多種實現(xiàn)的類型提供了兩種機制:接口和抽象類。
- 兩種機制的最明顯區(qū)別是抽象類允許包含某種方法的實現(xiàn),而接口不允許。
- 一個更重要的區(qū)別:為實現(xiàn)有抽象類定義的類型,實現(xiàn)的類必須是抽象類的子類。Java只允許單一繼承,因此抽象類作為類型的定義受到了極大的限制。
現(xiàn)有的類可以很容易的被更新以實現(xiàn)新的接口。所有需要做的工作是增加還不存在的方法并在類的聲明中增加一個implement語句。
接口是定義混合類型(mixins)的理想選擇。mixin是這樣的類型:除了它的“基本類型(primary type)”外,類還可以實現(xiàn)額外的類型,以表明它提供了某些可選的功能。
接口允許非層次類型框架的構(gòu)造。對于組織某些事物,類型層次是極好的選擇,但有些事物不能清楚地組織成嚴(yán)格的層次。
接口通過使用封裝類方式,能夠獲得安全、強大的功能。如果使用抽象類定義類型,那么程序員在增加功能是,除了使用繼承外別無選擇,而且得到的類與封裝類相比功能更差、更脆弱。
盡管接口不允許方法實現(xiàn),但使用接口定義類型并不妨礙給程序員提供實現(xiàn)上的幫助。可以通過抽象的構(gòu)架實現(xiàn)類與希望輸出的所有重要的接口配合,從而將接口與抽象類的優(yōu)點組合在一起。
使用抽象類定義具有多個實現(xiàn)的類型與使用接口相比有一個明顯優(yōu)勢:演進抽象類比演進接口更容易。如果在以后的版本重,需要給抽象類增加新方法,那么總可以增加一個包含正確的缺省實現(xiàn)的具體方法。此時所有現(xiàn)存的該抽象類的實現(xiàn)都會具有這個新的方法。
- + - 靜態(tài)成員優(yōu)于非靜態(tài)的
-
嵌套類(nested class)是一種定義在其他類內(nèi)部的類。嵌套類應(yīng)該僅僅為包容它的類而存在。
+ - 有四種類型嵌套類:
- 靜態(tài)成員類(static member class)
-
靜態(tài)成員類是最簡單的嵌套類。它最好被看做是普通的類碰巧被聲明在其他的類的內(nèi)部。它對所有封閉類中的成員有訪問權(quán)限,甚至那些私有成員。靜態(tài)成員類的一種通常用法是作為公共的輔助類,僅當(dāng)和它的外部類協(xié)作時才有意義。
如果聲明了一個不要求訪問封閉實例的成員類,切記要在它的聲明里使用static修飾符,把成員類變?yōu)殪o態(tài)的。 私有靜態(tài)成員類的一般用法是用來表示它們的封閉類對象的組件。
- 非靜態(tài)成員類(nonstatic member classer)
- 匿名類(anonymous class)
-
匿名類的行為與靜態(tài)的或非靜態(tài)的成員類一樣,依賴于它們出現(xiàn)的位置:如果它們出現(xiàn)在非靜態(tài)的上下文中,則具有封閉實例。匿名類僅在代碼中唯一的一點被實例化才能被使用,由于匿名類沒有名字,因此僅在被實例化后不需要再被訪問的情況下才適合使用。
過長的匿名類會傷害程序的可讀性。 //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(); } }
匿名類另一種通常用法是創(chuàng)建過程對象(process object),例如Thread、Runnable或者TimerTask實例。
第三種常用法是在靜態(tài)工廠方法內(nèi)使用。
第四種用法用在復(fù)雜的類型安全枚舉類型——要求給每個實例提供單獨的子類——的公共靜態(tài)的final域初始化程序中。
- 局部類(local class)
- 在局部變量可以聲明的地方都可以聲明局部類,它們同樣遵守作用域規(guī)則,性質(zhì)跟匿名類一樣。
除了第一種,其他的三種類都被稱為內(nèi)部類(inner class)
四種嵌套類,每種都有自己的用處。如果嵌套類在單一的方法之外可見,或是太長而不合適使用在一個方法內(nèi),那么使用成員類。如果成員類實例需要它的封閉類的引用,那么使它成為非靜態(tài)的;否則為靜態(tài)的。如果類存在于方法內(nèi)部,那么如果你僅需要在唯一一個位置創(chuàng)建實例,并且已存在刻化該類的類型,則使它成為匿名類;否則,用局部類。
- 對象的通用方法
- + - 重載equals時要遵守通用約定
- + - 如果滿足下列條件,就不要重載equals
-
每個類實例本質(zhì)上是唯一的。
不關(guān)心類是否提供了“邏輯意義的等同”(logical equality)測試。例如java.util.Random本來可以重載equals方法,用以檢查兩個Random實例是否會產(chǎn)生相同的隨機數(shù)序列,但設(shè)計者不認(rèn)為客戶會需要或想要這個功能。這種情況下,使用從Object繼承的equals實現(xiàn)就夠了。
超類已經(jīng)重載了equals,而從超類繼承的行為適合該類。
類是私有的或包內(nèi)私有(package-private)的,而且可以確定它的equals方法永遠不會被調(diào)用。
- 當(dāng)類有邏輯上的等同意義而不僅僅是對象意義上的等同,而且超類沒有重載equals方法以實現(xiàn)期望的行為,這時才需要重載。
- + - equals方法實現(xiàn)了相等關(guān)系(equivalence relation)
- 自反性(reflective):對于任意的引用值x,x.equals(x)總是返回true。
- 對稱性(symmetric):對于任意的引用值x、y,如果y.equals(x)返回true,x.equals(y)總返回true。
- 傳遞性(transitive):對于任意的引用值x、y、z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)總是返回true.
- 一致性(consistent):對于任意的引用值x、y,如果對象中用于equals比較的信息沒有修改,那么對x.equals(y)的多個調(diào)用,要么一致為true,要么一致為false。
- 對于任何非空引用值x,x.equals(null)總是返回false。
- + - 為實現(xiàn)高質(zhì)量的equals方法,下面提供一些方法
-
用"=="操作符檢查是否參數(shù)是對該對象的引用。
用instanceof操作符檢查是否參數(shù)是正確的類型。 public boolean equals(Object o) { if(!(o instanceof SomeClass)) return false; ... }
把參數(shù)映射到正確的類型。
對類中每一個“主要的”(significant)域,檢查是否參數(shù)中的域與對象中的相應(yīng)的域匹配。
完成equals方法時,問自己3個問題:它是否是對稱的、傳遞的、一致的。
- + - 實現(xiàn)equals方法應(yīng)該注意的地方
-
在重載equal方法時要重載hashCode方法。
不要使自己聰明過頭。把任何的同義形式考慮在比較的范圍內(nèi)一般是糟糕的想法,例如File類不應(yīng)該與指向同一文件的符號鏈接進行比較,實際上File類也沒有這樣做。
不要設(shè)計依賴于不可靠資源的equals方法。
不要將equals聲明中的Object替換為其他類型。程序員編寫出形如下面所示的equals方法并不少見,它會讓人摸不清頭腦:所設(shè)計方法為什么不能正確工作: public boolean equals(Myclass o) { ... } 問題出在這個方法沒有重載(override)參數(shù)為Object類型的Object.equals方法。而是過載(overload)了它。這在正常的equals方法中,又提供了一個“強類型”的equals方法。
- + - 重載equals時永遠要重載hashCode
-
一定要在每一個重載了equals的類中重載hashCode方法。不這樣做會違背Object.hashCode的一般約定,并導(dǎo)致你的類與所有基于散列的集合一起作用時不能正常工作,這些集合包括HashMap、HashSet和Hashtable。
不重載hashCode方法違背了java.lang.Object的規(guī)范:相等的對象必須有相等的散列碼。兩個截然不同的實例根據(jù)類的equals方法也許邏輯上是等同的,但對于Object類的hashCode方法,它們就是兩個對象,僅此而已。因而對象的hashCode方法返回兩個看上去是隨機的數(shù)值,而不是約定中要求的相等的值。
好的hash函數(shù)傾向于為不相等的對象生成不相等的hash碼。理想的情況下,hash函數(shù)應(yīng)該把所有不相等的實例的合理集合均一地分布到所有可能的hash值上去。達到理想狀態(tài)很難,但是下面有一種相對合適的方法
1.保存某個非0常數(shù)如17,到名為result的int類型變量中
2.對對象中每個“主要域”f,(每個域由equals方法負(fù)責(zé)),做下面的工作
- a.為域計算int型的hash碼c
-
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é)果進一步處理。
vi.如果域是對象引用,而且這個類的equals方法又遞歸地調(diào)用了equals方法對域進行比較,那么對這個域遞歸地調(diào)用hashCode方法。如果需要一種更復(fù)雜的比較方式,那么先為這個域計算出“范式表示”,然后在該“范式表示”上調(diào)用hashCode方法。如果域為null,則返回0。
vii.如果域是數(shù)組,則把每個元素作為分離的域?qū)Υ?。即遞歸地使用這些規(guī)則,為每個“主要元素”計算hash碼。然后用2.b所示方法復(fù)合這些值。
- b.把步驟a中計算出的hash碼c按如下方式與result復(fù)合: result = 37*result + c;
3.返回result。
4.完成hashCode方法后,測試是否相同的實例會有相同的hash碼,如果不是,找到原因,修正問題。
- + - 永遠要重載toString
- 為類提供一個好的toString實現(xiàn)可以使類使用起來更加賞心悅目。
- 實際使用中,toString方法應(yīng)該返回包含在對象中的所有令人感興趣的信息。
- 無論是否指明格式,都要把你的意圖清楚地文檔化出來。
- + - 謹(jǐn)慎的重載clone
- 為了實現(xiàn)Cloneable接口,會產(chǎn)生一種古怪的機制:不通過調(diào)用構(gòu)造函數(shù)卻創(chuàng)建了一個對象。
- 實現(xiàn)對像拷貝的精巧的方法是提供一個拷貝構(gòu)造函數(shù)(copy constructor)。拷貝構(gòu)造函數(shù)及它的靜態(tài)工廠變形與Cloneable/clone方法相比有很多好處
- 它們不依賴于那種有風(fēng)險的蹩腳的對象創(chuàng)建機制;
- 不需要遵守由糟糕的文檔規(guī)范的規(guī)約;
- 不會與final域的正常使用產(chǎn)生沖突;
- 不要求客戶不必要地捕獲被檢查的異常;
- 給客戶提供了一種類型化的對象。
- 方法
- + - 檢查參數(shù)的有效性
-
如果方法沒有對參數(shù)做檢查,會出現(xiàn)幾種情形。
- 方法可能在執(zhí)行中間失敗退出并給出含糊的異常。
- 更差的是,方法能正常返回,并計算了錯誤結(jié)果。
對那些不被方法使用但會被保存以供使用的參數(shù),檢查有效性尤為重要。
一種重要的例外是有效性檢查開銷高,或者不切實際,而且這種有效性檢查在計算的過程中會被隱式地處理的情形。
總的來說,每次在設(shè)計方法或設(shè)計構(gòu)造函數(shù)時,要考慮它們的參數(shù)有什么限制。要在文檔中注釋出這些限制,并在方法體的開頭通過顯示的檢查,對它們進行強化。養(yǎng)成這樣的習(xí)慣是重要的,有效性檢查所需要的不多的工作會從它的好處中得到補償。
- + - 使用保護性拷貝
-
必須在客戶會使用一切手段破壞類的約束的前提下,保護性地設(shè)計程序。
 下面的類的目的是表示非可變的時間周期: //Broken "immutable" time period class public final class Period { private final Date start; private final Date end;
public Period(Date start, Date end) { if(start.compareTo(end) >0 ) throw new IllegalArgumentException(start+" after "+end); this.start = start; this.end = end; }
public Date start() { return start; } public Date end() { return end; }
... } 乍看上去,這個類是非可變的,并通過執(zhí)行周期的起始點不會落在周期終止點之后的判斷,增強了類的約束。然而,如果Date是可變的,這種約束很容易被違背: // Attack the internals of a Period instance Date start = new Date(); Date end = new Date(); Period p = new Period(start,end); end.setYear(78); //Modifies internals of p!
為了保護Period實例的內(nèi)部細節(jié)免于這種攻擊,對構(gòu)造函數(shù)的每一個可變參數(shù)使用保護性拷貝是必要的。
 使用副本代替初始值作為Period實例的組件: //Repaired constructor - make defensice copies of parameters public Period(Date start, Date end){ this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if(this.start.compareTo(this.end)>0) throw new IllegalArgumentException(start+" after "+end): }
保護性拷貝要在參數(shù)的有效性檢查的前面,并且有效性檢測要在副本而不是初值上執(zhí)行。
 盡管替代構(gòu)造函數(shù)成功防止了前面的攻擊,但改變一個Period實例仍然是可能的,因為訪問器對它的可變的內(nèi)部細節(jié)提供了訪問能力。 //Second attack on the internals of a Period instance Date start = new Date(); Date end = new Date(); Period p = new Period(start,end); p.end().setYear(78); //Modifies internals of p!
 為了防止第二種攻擊,簡單地修改訪問器,返回可變內(nèi)部域的保護性拷貝即可: //Repair accessors - make defensive copies of internal fields public Date start() { return (Date) start.clone(); } public Date end() { return (Date) end.clone(); }
只要可能,就應(yīng)該使用非可變對象作為對象的組件,以便不再關(guān)心保護性拷貝問題。
- + - 認(rèn)真設(shè)計方法簽名
- 認(rèn)真地給方法選名字。名字要永遠遵守標(biāo)準(zhǔn)的命名慣例。
- 不要過于追求提供便利的方法。對接口這一點是千真萬確的,接口中方法太多會使實現(xiàn)程序和用戶使用時變得復(fù)雜。
- 避免長參數(shù)列表。三個參數(shù)的使用就該作為最大值。類型相同的長參數(shù)序列尤其有害。
有兩種技術(shù)可以大幅縮短常參數(shù)列表。一種技術(shù)是將一個方法分成多個方法實現(xiàn),其中每個方法僅需要參數(shù)類表的一個子集。
第二種技術(shù)是創(chuàng)建助手類,用來保持參數(shù)集合。
- 在參數(shù)類型的使用上,接口優(yōu)于類。
- 謹(jǐn)慎地使用函數(shù)對象。
- + - 謹(jǐn)慎地使用過載
-
下面是一個意圖良好的嘗試,按照是否是集、列表還是其他種類的集合把集合分類: //Broken - incorrect user of overloading! public class CollectionClassifier { public static String classify(Set s) { return "Set"; } public static String classify(List l) { return "List"; } public static String classify(Collection c) { return "Unkown Collection"; } public static void main(String[] args) { Collection[] tests = new Collection[] { new HashSet(), new ArrayList(), new HaspMap().values() };
for(int i=0;i<tests.length;i++) System.out.println(classify(tests[i])); } } 也許認(rèn)為這個程序會依次打出Set、List和Unkown Collection,但實際是打印3次Unkown Collection。這是因為classify方法被過載了,選擇使用哪個調(diào)用的決定是在編譯期做出的。
改正的方法是用一個顯式instanceof測試實例的方法代替classify中的三個過載方法: public static String classify(Collection c) { return (c instanceof Set? "Set" :(c instanceof List ? "List" : "Unkown Collection")); }
一種安全、保守的策略是永遠不要導(dǎo)出兩個具有相同數(shù)目參數(shù)的過載方法。
- + - 返回0長度的數(shù)組而不是null
-
下面的方法比較常見: private List CheesesInStock = ...;
public Cheese[] getCheeses() { if(cheesesInStock.size() == 0) return null; ... } 沒有理由需要對無奶酪可買這種情況做特殊的處理。著需要客戶方提供額外代碼處理null的返回值,例如: Cheese[] cheeses = shop.getCheeses(); if (cheeses != null && Array.asList(shop.getCheeses()).contains(Cheese.STILTON)) System.out.println("Jolly good, just the thing."); 而不是: if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON)) System.out.println("Jolly good, just the thing.");
幾乎每次返回null而不是返回0長度數(shù)組的方法時,都需要這種多余地處理。返回null是易出錯的,因為寫代碼的程序員可能會忘記設(shè)計處理返回null的特殊情形的代碼。
正確的做法是: private List cheeseInStock = ...; private final static Cheese[] NULL_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() { return (Cheese[]) cheesesInStock.toArray(NULL_CHEESE_ARRAY); }
- + - 通用編程
- + - 最小化局部變量作用域
- 通過最小化局部變量的作用域,可以增加代碼的可讀性和可維護性,減少出錯可能。
- 最小化局部變量的最有效的方式是在它第一次被使用時聲明。
- 幾乎每一個局部變量聲明都應(yīng)該包含一個初始化器(initializer)。
- 最小化局部變量的最后一個技術(shù)是使方法小而集中。
- 需要確切答案時,不要使用float或double類型
- float和double類型特別不適合貨幣計算。
假設(shè)手中有$1.03,花掉0.42后還剩多少錢呢? System.out.println(1.03 - .42); 不幸的是,它會打印0.6100000000000001。
解決這個問題的正確方法是使用BigDecimal、int和long類型進行貨幣計算。
- 使用BigDecimal有兩個缺點。
- + - 盡量避免使用串
- 串是值類型的糟糕的替代物。
- 串是可枚舉類型的糟糕的替代物。
- 串是集合類型的糟糕的替代物
- 串是capabilities(能力表)的糟糕的替代物
- + - 了解串并置的性能
- 為連接n個串重復(fù)地使用串合并操作符需要n的二次方時間。
- 為了獲得可接受的性能,可以使用StringBuffer代替String。
- 通過接口訪問對象
-
如果存在合適的接口類型,那么參數(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í)慣,程序就會有更好的擴展性。當(dāng)希望轉(zhuǎn)換實現(xiàn)時,需要做的全部工作就是改變構(gòu)造函數(shù)中類的名字。
- 謹(jǐn)慎地做優(yōu)化
- 人們通常都把計算機的罪歸咎于效率問題(甚至是不必要的獲得的效率),而不去懷疑任何其他的原因——甚至包括盲目地做傻事?!猈illiam A.Wulf
- 不要計較微小效率的得失,在97%的情況下,不成熟的優(yōu)化是一切罪惡的根源。 ——Donald E.Knuth
- 做優(yōu)化時,要遵循兩條原則:
原則1 不要做優(yōu)化 原則2(僅對專家) 還是不要做優(yōu)化——也可以這么說:在絕對清楚的、未經(jīng)優(yōu)化的方案之前,不要做優(yōu)化。 ——M.A.Jackson
|