effective java 筆記2

來源:互聯網
上載者:User

from:http://blog.csdn.net/ilibaba/archive/2009/01/13/3769578.aspx

NO.7 在改寫equals方法時請遵守通用約定
下列情況是不需要改寫equals方法的:

1。同一個類的不同執行個體本質上是唯一的
2。不關心該類是否提供了邏輯相等的功能。

3。父類已經改寫過equals方法,對於子類來說,繼承過來的equals方法已經是最合適的了。

4。一個類是私人的或者是包可見的,且確定它的equals方法不會被調用。

對於需要改寫equals方法的時候,應該遵守如下約定:

1。自反性,即x.equals(x)為true.

2。對稱性,即若且唯若x.equals(y)為true時y.equals(x)也一定為true。

3。傳遞性,即對任意的x,y,z,如果x.equals(y)為true,並且y.equals(z)也為true,那麼x.equals(z)也必須為true.

4。一致性,即對於任意的x,y,如果x,y都沒有被修改的話,那麼多次調用x.equals(y)要麼一致地返回ture,要麼一致地返回false.

5。對於非空的引用x,x.equals(null)一定要返回false.

改寫equals方法時的建議:

1。用==操作符檢查實參是否為指向對象的同一個引用。

2。使用instanceof檢查實參是否是正確的類型。

3。在2的基礎上,把實參轉換成正確的類型。

4。檢查實參的域與當前對象的域值是否相等。

5。編寫完equals方法後,檢查是否滿足等價關係。
例如:

  1. public boolean equals(Object o)
  2. {
  3. if(o== this) return true;
  4. if(!(o instanceof xxxx) return false;
  5.        xxx in = (xxx)o;
  6. return ……..
  7. }

改寫equals方法的告誡:

1、不要企圖讓equals方法做太多事。

2、不要使equals依賴不可靠的資源,否則會違背一致性。

3、不要將equals中的對象裝換為其他的類型。要注意的是,不要提供這樣的方法public boolean equals(MyClass o)這樣是overload並不是override Object的equals方法。實參必須為Object類型,只是overload了equals方法,如果兩者返回同樣的結果是可以接受的,但是這種做法不值得。

總結:不用重寫equals方法就盡量不要去找麻煩,確實需要改寫equals方法時,遵守通用約定,因為對象會在程式中不停的傳遞,所以可能會導致程式運行不正常,甚至崩潰而很難找到程式崩潰的原因。總之,還是遵守約定吧!

 

NO.8 改寫equals方法時必須覆蓋hashCode方法
       這點必須切忌,不然在你和hash-based集合打交道的時候,錯誤就會出現了。關鍵問題在於一定要滿足相等的對象必須要有相等的hashCode。如果你在PhoneNumber類中覆蓋了equals方法,但是沒有覆蓋hashCode方法,那麼當你做如下操作的時候就會出現問題了。

  1. Map m = new HashMap();
  2. m.put(new PhoneNumber(408,863,3334),”ming”)

當 你調用m.get(new PhoneNumber(408,863,3334))的時候你希望得到ming但是你卻得到了null,為什麼呢因為在整個過程中有兩個 PhoneNumber的執行個體,一個是put一個是get,但是他們兩個邏輯相等的執行個體卻得到不同的hashCode那麼怎麼可以取得以前存入的ming 呢。

 

NO.9 總是要改寫toString()方法
        在Object的toString方法返回的形式是Class的類型加上“@”加上16進位的hashcode,非常難以理解。最好在自己的類中提供toString方法更好的表述執行個體的資訊,不然別人怎麼看得明白呢。
        在實際應用中,toString方法應該返回對象中包含的所有令人感興趣的資訊。同時,最好在程式中提供一個相匹配的建構函式或者靜態Factory 方法,便於程式員在對象和它的字串表示之間進行來迴轉換。
       在實現toString方法的時候,必須要做出是否在文檔中指定傳回值的格式的決定。指定格式可以被用來做為一種標準的,無二意性的表達形式,但這樣也會使字串的表示嵌入到永久資料中,如果以後改變了表達形式,則會影響到系統的代碼和資料。不管你是否決定指定格式,都應該在文檔中清晰的表明自己的意圖。為toString傳回值中所包含的資訊,提供一種編程訪問途徑,用來擷取toString方法返回字串中的資訊,可以避免程式員自己去解析字串而導致的錯誤。

 

NO.10 謹慎地改寫clone(clone方法詳解請參見java clone方法使用詳解)

      一個對象要想被Clone,那麼要implements Cloneable介面,實現clone()方法,如果你不實現這個介面的話,調用clone方法的 時候會出現CloneNotSupportedException,這就是作者叫做mixin的介面類型。通常clone()方法可以這樣覆蓋

  1. public Object clone() 
  2. try
  3. return super.clone(); 
  4. catch(CloneNotSupportedException e) 
  5. {} 

但是當你要clone的類裡面含有可修改的引用欄位的時候,那麼你一定要把整個類的藍圖進行複製,如果對你clone得到的對象進行修改的時候還會影響到原來的執行個體,那麼這是不可取的。所以應該這樣clone():

  1. public Object clone() throws CloneNotSupportedException 
  2.        Stack Result  = (Stack)super.clone(); 
  3.        Result.elements = (Object[])elements.clone(); 
  4.        Return result; 

其中elements是stack類中可修改的引用欄位,注意如果elements是final的話我們就無能為力了,因為不能給他重新賦值了.其實如果不是必須的話,根本就不用它最好。

      注意深複製和淺複製的問題。為了實現深複製,實現Cloneable介面的類應該首先調用super.clone,然後修正任何需要修正的域,拷貝任何“深層結構”的可變對象。

      clone方法如果實現得不當會給系統帶來隱藏的bug,如果非要使用類似的功能最好的辦法是提供某些其他的途徑(拷貝建構函式或者提供一個靜態Factory 方法來替代建構函式)來替代對象的拷貝,或者乾脆不提供這樣的能力。

      Cloneable有很多問題,所以安全的說,其他的介面不應該擴充(extend)這個介面,並且為了繼承而設計的類也不應該實現(implement)這個介面。

 

NO.11 考慮實現Comparable介面

      compareTo方法是java.lang.Comparable介面中的唯一方法,它允許進行簡單的相等比較,也允許執行順序比較,一個類實現了comparable介面就表明他的執行個體具有內建的排序關係。Java平台庫中所有的值類都實現了Comparable。將當前對象與指定對象進行順序比較的時,返回負整數,0或者正整數(<、=、>)。

     compareTo方法應遵守如下限制條件:自反性、對稱性、傳遞性和非空性的限制條件。在實現數值比較的compareTo方法時還要防止範圍溢出的情況(例如i為很大的正數,j為很大的負數,使用i-j判斷大小就可能溢出返回一個負數)。

     違反compareTo約定的類也會破壞其他依賴於比較關係的類,包括TreeSet,TreeMap,以及工具類Collections和Arrays。這些內部包含搜尋和排序演算法的類。

    編寫compareTo和equals方法類似,但存在幾個不同點。compareTo不需要檢查實參的類型,如果類型不合適應該拋出ClssCastException,如果為null應該拋出NullPointerException;域的本身是順序比較而不是相等比較。

 

NO.12 使類和成員的可訪問能力最小化

好的模組設計應該盡最大可能封裝好自己的內部資訊,隱藏資訊可以把模組之間的耦合程度降到最低。開發得以並行,無疑這將加快開發的速度,便於系統地維護。Java中通過存取控制符來解決這個問題。

  • public表示這個類在任何範圍都可用。
  • protected表示只有子類和包內的類可以使用
  • default(private-package,即不寫)表示在包內可用
  • private表示只有類內才可以用

在設計的時候應該儘可能的使每一個類或者成員不被外界所訪問。如果一個類只是被另一個類使用,那麼應該考慮把它設計成這個類的內部類。子類在override超類的函數時存取層級不能低於超類的,一個具體例子就是實現介面的方法時,都必須是public的,因為介面中的方法都是public的。

公有類應該儘可能少地包含共有的域,包含公有可變域的類不是安全執行緒的。這條規則有個例外,就是通過共有靜態final域來暴露類的常量是允許的。這樣的域的名字由大寫字母組成,單詞之間用底線隔開。不過必須保證這些欄位要麼是基礎資料型別 (Elementary Data Type),要麼引用指向的對象是不可修改的。不然他們將可能被修改。例如下面的定義中data就是不合理的,其他人可以改變數組中的內容,有安全性漏洞,後面兩個沒有問題:

  1. public class Con 
  2. public static final int[] data = {1,2,3};// it is bad
  3. public static final String hello = "world"; 
  4. public static final int i = 1; 
  5. }  

注意:非零長度的數組總是可變的,所以具有公有靜態final數組域幾乎總是錯誤的。

解決這個安全隱患的方法有兩種:

①將公有方法替換為一個私人數組,以及一個公有的非可變列表

  1. private static final Type[] PRIVATE_VALUES = {...}; 
  2. public static final List VALUES = Collections.unmodifiableLis(Arrays.asList(PRIVATE_VALUES)); 

②把公有的數組替換為一個公有的方法,它返回私人數組的一份拷貝。這個方法會犧牲一點效能。

  1. private static final Type[] PRIVATE_VALUES = {...}; 
  2. public static final Type[] values(){ 
  3. return (Type[])PRIVATE_VALUES.clone(); 

總之,應該防止把任何雜散的累介面和成員變成API的一部分,除了公有靜態final域的特殊情形之外,公有類不應該包含公有域,並且確保有靜態final域所引用的對性是不可變的。

 

NO.13 支援非可變性

一個非可變類的執行個體不能被修改,每個執行個體中包含的所有資訊都必須在該執行個體被建立的時候就提供出來,並且在對象的整個生存期內固定不變。例如String類,BigInteger、BigDecimal都是非可變類。存在理由:比可變類更加易於設計、實現、使用,不容易出錯,所以安全。

為了使一個類成為非可變類,要遵循下面五條規則:

①不要提供任何會修改對象的方法;

②保證沒有可被子類改寫的方法;

③使所有的域都是final的;

④使所有的域都成為私人的;

⑤保證對於任何可變組件的互斥訪問。(如果一個類指向可變對象的域,則必須確保該類的客戶無法活得指向這些對象的引用,並且永遠不要用客戶提供的對象引用來初始化這樣的域,也不要在任何一個存取方法中返回該對象的引用);

以上規則比真正的要求強了一點,為了提高效能可以有所方式,如:保證沒有一個方法能夠對對象的狀態產生外部可見的改變,許多非可變的類擁有一個或者多個非final的冗餘域,把一個開銷昂貴的計算結果緩衝在這些域中。
非可變對象本質上是安全執行緒的,它們不要求同步。非可變對象可以被自由地共用。你不僅可以共用非可變對象,甚至也可以共用它們的內部資訊。非可變對象為其他對象--無論是可變的還是不可變的--提供了大量的構件。

非可變類真正唯一的缺點是,對於每一個不同的值都要求一個單獨的對象。String就是這樣的。通常有個解決的辦法就是提供一個協助類來彌補,例如StringBuffer類。

使一個類成為非可變類有如下三種方法:

①將一個類聲明為final類型的;

②讓該類中的每一個方法都成為final,而不是讓整個類是final的,這種方法的好處在於其子類不可以繼承,但是可以擴充新的方法;

③把類的建構函式聲明為私人的或者包級私人的,增加靜態Factory 方法,來代替公有的建構函式;(該方法雖然不常用,但卻是最值得推薦的)
如果選擇讓自己的非可變類實現Serializable介面,並且包含一個或多個指向可變對象的域,那麼,你必須提供一個顯示的readObject或者readResolve方法。預設的readObject方法使攻擊者可以從非可變類建立可變的執行個體。

總之,除非有好的理由,否則類應該設計成非可變的。如果一個類不能被做成非可變類,那麼你仍然應該儘可能地限制它的可變性。建構函式應該建立完全初始化的對象,所有的約束關係應該在這時候建立起來,不應該把“只構造了一部分的執行個體”傳遞給其他的方法,在建構函式之外提供公有的初始方法。

 

NO.14 複合優先於繼承
       實現代碼重用最重要的辦法就是繼承,但是繼承破壞了封裝,導致軟體的鍵壯性不足。如果子類繼承了父類,那麼它從父類繼承的方法就依賴父類的實現,一旦他改變了會導致不可預測的結果。如果子類和超類在不同的包中,並且超類並不是為了擴充而設計的,那麼繼承會導致脆弱性。作者介紹了InstrumentedHashSet作為反例進行說明,原因就是沒有明白父類的方法實現。作者給出的解決辦法是通過複合來代替繼承,尤其是當存在一個適當的介面來實現一個封裝類的時候,用封裝類和轉寄方法來解決問題。把想擴充的類作為本類的一個private final成員變數。把方法參數傳遞給這個成員變數並得到傳回值。這樣做的缺點是這樣的類不適合回調架構。繼承雖然好,我們卻不應該濫用,只有我們能確定它們之間是is-a得關係的時候才使用。

 

NO.15 要麼專門為繼承而設計,並給出文檔說明,要麼禁止繼承

對並沒有文檔說明的類進行繼承是非常危險的,它的公有方法有可能被改變。在設計一個專門用來繼承的類時必須注意以下幾點(不適用於final類):

①必須精確地描述改寫每個方法帶來的影響,雖然這樣的描述違法了文檔格言“好的API文檔應該描述一個方法做了什麼工作,而不是描述它如何做”,但這也是繼承破壞了程式的封裝性而導致的。

②允許繼承的類的建構函式一定不能調用可被改寫的方法,無論是直接進行還是間接進行。因為超類的建構函式會在子類的建構函式之前運行,所以子類中override版本的方法將會在子類的建構函式運行之前就被調用。如下邊出現的錯誤:

  1. public class SuperClass{ 
  2. private final Date date; 
  3. public SuperClass() { 
  4.      m();
  5.      } 
  6. public void m() { } 
  7. final class Sub extends SuperClass {
  8.     private final Date date;
  9.     public Sub () { 
  10.         date=new Date();
  11.     } 
  12.     public void m() {  //overrides SuperClass.m,invoked by constructor SuperClass()
  13.         System.out.println(date); 
  14.     } 
  15. public static void main(String[] args) {  
  16.         Sub s = new Sub();  
  17.         s.m();  
  18.     } 

在這個程式中,第一次列印會出現null,因為m被建構函式SuperClass調用的時候,Sub還沒有機會初始化date域。

同樣的問題會出現在clone和readObject這兩個類似建構函式的方法上,所以clone和readObject都不可以調用一個可改寫的方法,無論直接還是間接。

“建構函式一定不能調用可被改寫的方法”的實現方式:把每個可改寫的方法的代碼體移到一個私人的“輔助方法”中,並且讓每個可改寫的方法調用他的私人輔助方法,然後用“直接調用可改寫方法的私人輔助方法”來代替“可改寫方法的每個自用調用”。

在為了繼承而設計類的時候,不推薦實現Cloneable和Serializable介面。clone和readObject方法在行為上和建構函式相似——“一定不能調用可被改寫的方法,無論是直接進行還是間接進行”,對於readObject方法,子類中改寫版本的方法將子類的狀態被還原序列化之前運行,而對於clone方法,改寫版本的方法將在子類的clone方法有機會修正被複製對象的狀態之前被運行,無論哪種情形,它們都會不可避免的導致程式失敗。

實現Serializable,並且類中有readResolve或者writeReplace方法的時候,必須使它們成為protected而不是private,因為private的函數不能被子類調用。

對於既不是final類,也不是為了子類化而設計和編寫文檔的普通類而言,防止出現問題的最好方法是禁止子類化,方法有兩種:

①把這個類聲明為final的。

②把所有的建構函式變成私人的,或者包級私人的,並增加一個公有的靜態Factory 方法來代替建構函式。

繼承的另一個替代方案就是利用封裝類模式,具體參見NO.14 複合優先於繼承

 

NO.16 介面優於抽象類別

介面和抽象類別的區別:

①抽象類別允許包含某些方法的實現,而介面是不允許的;

②一個類要實現抽象類別,它必須成為抽象類別的一個子類,而實現介面的類只要定義了所要求的方法,並遵守通用的約定,不管這個類位於類層次的哪個地方;

介面可以構造出非層次結果的類型架構,比如一個介面可以繼承多個其他的介面。還可以安全地增加一個類的功能。

當然,也可以把介面和抽象類別的有點結合起來,對於你期望匯出的每一個重要的介面,先提供一個抽象的骨架實作類別,這樣,介面的作用仍然是定義類型,骨架實作類別負責所有與介面實現相關的工作。

抽象類別也有明顯的優勢,它可以在一個類的後續的版本中方便的增加一個新的方法,但不影響到其他相關的類,而介面則做不到這一點。

總之,介面通常是定義具有多個實作類別型的最佳途徑(例外的情況:當演化的容易性比靈活性和功能更為重要的時候,應該使用抽象類別來定義類型,但也必須理解抽象類別的局限性,並確保可以接受這些局限性)。如果已經匯出了一個重要的介面,那麼,也應該考慮同時提供一個骨架實作類別。最後,應該儘可能的謹慎設計所有的公有介面,並通過編寫多個實現來對它們進行全面的測試。

 

NO.17 介面只是被用於定義類型

介面只是用來定義一個類型,不要把介面用來做其他的事情(如在介面中定義常量,這種常量介面模式是對介面的不良使用)。

如果要匯出常量,可以有以下幾種方式:

①如果這些常量與某個已有的類或者介面有著緊密的聯絡,則可以把常量添加到這個類或者介面中。

②定義一個型別安全的枚舉類,把這些常量看做枚舉類型的成員。

③使用一個可以執行個體化的工具類(建構函式設為private)來匯出這些常量。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.