碼處高效:覆蓋 equals() 時切記要覆蓋 hashCode()

關注公衆號回覆002,裏面有你想要的一切java


在每一個覆蓋了 equals 方法的類中,都必須覆蓋 hashCode 方法。若是不這樣作的話,就會違反 hashCode 的通用約定,從而致使該類沒法結合全部的給予散列的集合一塊兒正常運做。這類集合包括 HashSet、HashMap,下面是Object 的通用規範:程序員

  • 在應用程序的執行期間,只要對象的 equals 方法的比較操做所用到的信息沒有被修改,那麼同一個對象的屢次調用,hashCode 方法都必須返回同一個值。在一個應用程序和另外一個應用程序的執行過程當中,執行 hashCode 方法返回的值能夠不相同。web


  • 若是兩個對象根據 equals 方法比較出來是相等的,那麼調用這兩個對象的 hashCode 方法都必須產生一樣的整數結果數組


  • 若是兩個對象根據 equals 方法比較是不相等的,那麼調用這兩個對象的 hashCode 方法不必定要求其產生相同的結果,可是程序員應該知道,給不相等的對象產生大相徑庭的整數結果,有可能提升散列表的性能。緩存

因沒有覆蓋 hashCode ,容易違反上面第二條的約定,即相等的對象必須擁有相同的 hashCode 散列值微信

根據類的 equals 方法,兩個大相徑庭的實例在邏輯上有多是相等的。可是根據 Object 的 hashCode 方法來看,它們僅僅是兩個大相徑庭的對象而已。所以對象的 hashCode 方法返回兩個看起來是隨機的整數,而不是根據第二個約定所要求的那樣,返回兩個相等的整數。app

例以下面這個例子:eclipse

public class PhoneNumber {
int numbersOne; int numbersTwo; int numbersThree;
public PhoneNumber(int numbersOne, int numbersTwo, int numbersThree) { this.numbersOne = numbersOne; this.numbersTwo = numbersTwo; this.numbersThree = numbersThree; }
@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber that = (PhoneNumber) o; return Objects.equals(numbersOne, that.numbersOne) && Objects.equals(numbersTwo, that.numbersTwo) && Objects.equals(numbersThree, that.numbersThree); }
public static void main(String[] args) { Map numberMap = new HashMap(); numberMap.put(new PhoneNumber(707,867,5309),"Jenny");
System.out.println(numberMap.get(new PhoneNumber(707,867,5309))); }}

此時,你可能但願 numberMap.get(new PhoneNumber(707,867,5309)) 會返回 "Jerry",但它實際上返回的是null 。這裏會涉及到兩個實例:第一個實例是第一次添加進入的 PhoneNumber , 它會被添加到一個桶中。由於沒有重寫 hashCode 方法,因此你取的時候是去另一個桶中取出來的 PhoneNumber 實例。因此天然兩個實例不相等,由於 HashMap 有一項優化,能夠將與每一個項相關聯的散列碼緩存起來,若是散列碼不匹配,也就再也不去檢驗對象的等同性。ide

修正這個問題很是的簡單,只要提供一個相等的散列碼就能夠了函數

@Overridepublic int hashCode() { return 42;}

上面這個 hashCode 方法是合法的。由於它確保了相等的對象老是具備一樣的散列碼。可是它也極爲惡劣,由於每一個對象都具備相同的散列碼。所以,多個具備相同散列碼的 HashMap 就會彼此連在一塊兒造成鏈表。它使得本該以線性時間運行的程序變成了以平方級的時間運行。

一個好的散列一般是 "爲不相等的對象產生不相等的散列碼"。這正是 hashCode 約定中的第三條含義。理想狀況下,散列函數應該把集合中不相等的實例均勻地分佈到全部可能的 int 值上。下面是一種簡單的解決辦法:

  1. 聲明一個 int 變量並命名爲 result,將它初始化爲對象中的第一個關鍵域散列碼 c.

  2. 對象中剩下的每個關鍵域 f 都完成如下步驟:

    爲該域計算 int 類型的散列碼 c:

    按照 下面的公式,把散列碼 c 合併到 result 中。

    result = 31 * result + c;
  3. 1)若是該域是基本類型,則計算 Type.hashCode(f),這裏的 Type 是集裝箱基本類型的類,與 f 的類型相對應


    2)若是該域是一個對象引用,而且該類的 equals 方法經過遞歸地調用 equals 的方式來比較這個域,則一樣爲這個域遞歸地調用 hashCode 。若是爲null ,則返回0


    3)若是該域是一個數組,則要把每個元素看成單獨的域來處理。也就是說,遞歸地應用上述規則,對每一個重要的元素計算一個散列碼,而後根據步驟2 . b中的作法把這些散列值組合起來。若是數組域中沒有重要的元素,可使用一個常量,但最好不要用0。若是數組域中的全部元素都很重要,可使用 Arrays.hashCode 方法。

  4. 返回result

寫完了以後,還要進行驗證,相等的實例是否具備相同的散列碼,能夠把上述解決辦法用到 PhoneNumber 中

@Overridepublic int hashCode() { int result = Integer.hashCode(numbersOne); result = 31 * result + Integer.hashCode(numbersTwo); result = 31 * result + Integer.hashCode(numbersThree); return result;}

雖然上述給出了 hashCode 實現,但它不是最早進的。它們的質量堪比 Java 平臺類庫提供的散列函數。這些方法對於大多數應用程序而言已經足夠了。

Objects 類有一個靜態方法,它帶有任意數量的對象,併爲它們返回一個散列碼。這個方法名爲 hash 。你只須要一行代碼就能夠編寫它的 hashCode 方法。它們的質量也是很高的,可是,它的運行速度相對慢一些,由於它們會引起數組的建立,以便傳入數目可變的參數,若是參數中有基本類型,還須要裝箱和拆箱。例如:

@Overridepublic int hashCode(){ return Objects.hash(numbersOne,numbersTwo,numbersThree);}

若是一個類是不可變的,而且計算 hashCode 的開銷也大,那麼應該把它緩存在對象內部,而不是每次請求都從新建立 hashCode。你能夠選擇 "延遲初始化" 的散列碼。即一直到 hashCode 被第一次使用的時候進行初始化。以下:

private int hashCode;
@Overridepublic int hashCode() { int result = hashCode; if(result == 0){ result = Integer.hashCode(numbersOne); result = 31 * result + Integer.hashCode(numbersTwo); result = 31 * result + Integer.hashCode(numbersThree); hashCode = result; } return result;}

當你要重寫對象的 hashCode 方法時,下面這兩個約定我但願你能遵照:

  • 不要對 hashCode 方法的返回值作具體的規定,所以客戶端沒法理所固然地依賴它;這樣能夠爲修改提供靈活性。


  • 不要試圖從散列碼計算中排除掉一個對象的關鍵域來提升性能。

總而言之,每當覆蓋 equals 方法時都必須覆蓋 hashCode。不然程序將沒法正確運行。hashCode 方法必須遵照 Object 規定的通用約定,而且一塊兒完成必定的工做。將不相等的散列碼分配給不相等的實例。這個很容易實現,可是若是不想那麼費力,能夠直接使用 eclipse 或者 Idea 提供的 AutoValue 自動生成就能夠了。

<End>  


PS:原創不易,喜歡就點個在看 or 轉發朋友圈,這將是咱們最強的寫做動力。


好看的人才能點




本文分享自微信公衆號 - Java建設者(javajianshe)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索