equals和hashcode終極解答

爲什麼重寫equals就必須重寫hashcode?

我們必須先了解hash桶。水桶用來存放水,而hash桶用來存放多個hash值,hash算法負責將hash值分配到hash桶裏,而相同的hash值始終位於相同的桶內。當存儲元素時首先計算出hash值,然後找到對應的hash桶,把值放在該桶裏。查找元素時,同樣計算出hash值並找到對應的hash桶,隨後將桶內所有元素取出一一對比equals,即完成查找。
爲什麼要利用hash桶呢?這樣是爲了減少需要用equals比較的元素數量。想象下傳統的方案中我們將所有元素一一對比equals,而現在只需和一個hash桶內元素對比即可。
那如果不重寫hashcode,即可能出現equals相同的元素其hashcode不同。假如我們已經在set中插入了一個元素,現在插入一個跟它equals的元素,但由於兩者hashcode不同,因此被分配到不同的桶中,導致這個元素被添加進set,這違反了set的規則!
從上述可以看出,hashcode的作用是輔助equals的判斷,它主要考慮的是效率。

重寫equals

重寫equals具有許多陷阱,稍不小心就會違反下面這些原則:
- 自反性
- 相對性
- 傳遞性
- 永久性
- 任何非NULL對象不等於NULL
接下來我們先給出一個標準實現,然後一一解釋

    @Override
    public boolean equals(Object o) {
        //自檢查
        if(this == o )
            return true;

        //空檢查
        if (o == null)
            return false;

        //類型檢查和轉換
        if (getClass() != o.getClass())
            return false;

        Car car = (Car)o;

        // 成員比較
        return Objects.equals(name, car.name);
    }
  • 函數參數
    函數參數一定要爲object,考慮如下情況:
     Car a = new Car("my");
     Object b = new Car ("my");

     boolean answer = a.equals(b);

這裏answer將爲false。因此java的方法重載基於靜態類型,因此這裏調用的是Obeject.equals方法。當我們使用集合時會大量遇到這種情況。

  • 自校驗
    equals是一個被經常調用的基本方法,因此需要注意它的性能,自校驗可以極大的提高性能。

  • 空校驗
    空校驗保證上述最後一條原則,也避免了NullPointerException

  • 類型檢查和轉換
    這裏有兩種實現方式,我們採取的方式將導致所以子類equals返回false,這可能不是我們想要的結果,因爲有些子類僅添加功能而不增加成員變量,那麼它們應該允許和父類相等。對hibernate或者spring這些會在運行中產生子類的框架來講這點尤爲重要。因此,我們衍生出另一種做法採用instanceof

    if (!(o instanceof Car))
    return false;

    但需要注意的是這種做法會引起其他問題。例如SUV派生自Car同時添加了新的成員變量並覆蓋equals方法,那麼car.equals(suv)將返回true,而suv.equals(car)將返回false,這顯然違反了相對性。
    當然我們可以在suv的equals中檢查object是否爲car來避免這個問題,但這種做法違反了傳遞性,比如:

     Car a = new Car("my");
     SUV s1 = new SUV("my", "1");
     SUV s2 = new SUV("my", "2");

    a分別和s1以及s2滿足equals,但s1和s2不滿足。
    所以這裏有兩種做法:使用getclass同時記住所有的子類和父類不能equals;使用instanceof方法同時設置equals爲final

  • 成員比較
    基本類型使用==,引用類型使用equals,對於可能爲空的字段需要檢驗是否爲NULL。因此建議採用Objects.equals方法

    重寫hashcode

    重寫hashcode需要注意幾點:

  • 選擇字段
    不能選擇equals函數沒有采用的字段,這樣可能導致兩個equals的對象其hashcode卻不相同!換句話即採用equals字段的子集。

  • 一致性
    如果不小心將已經添加到hashset的對象作出了修改,由於hash值不會重新計算,hashset的內部結構也不會改變,因此接下來即使使用相equals的對象也無法查找到該元素!即使使用該元素本身也不行!所以我麼需要儘量使用不可變變量來計算hash值。

    計算過程

    把某個非0的常數值,比如17,保存在一個名爲result的int類型的變量中。
    對於對象中的每個域,做如下操作:
    爲該域計算int類型的哈希值c:
    如果該域是boolean類型,則計算(f?1:0)
    如果該域是byte、char、short或者int類型,則計算(int)f
    如果該域是long類型,則計算(int)(f^(f>>>32))
    如果該域是float類型,則計算Float.floatToIntBits(f)
    如果該域是double類型,則計算Double.doubleToLongBits(f),然後重複第三個步驟。
    如果該域是一個對象引用,並且該類的equals方法通過遞歸調用equals方法來比較這個域,同樣爲這個域遞歸的調用hashCode,如果這個域爲null,則返回0。
    如果該域是數組,則要把每一個元素當作單獨的域來處理,遞歸的運用上述規則,如果數組域中的每個元素都很重要,那麼可以使用Arrays.hashCode方法。
    把上面計算得到的hash值c合併到result中

發佈了23 篇原創文章 · 獲贊 15 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章