hashcode重寫

Wayne_Kdl發表於2019-12-10

這篇文章是以前寫的,從segmentfault搬運過來的,因為我的seg上只有一篇文章,孤零零的,所以搬過來。

我的第一篇文章

以前都在公司的文件裡寫,後來想想,還是自己找個地方記錄下來吧。 今天有個朋友問我hashcode的問題,記錄下來,並稍微讀下書尋求一點理論知識。

問題如下

有一個屬性都是字串的物件,想放入hashset中,要求,對某一個屬性,相同就能放入,不同就不能放入。

朋友的問題是,知道equals咋寫,但是不知道hashcode咋寫,沒有思路。

我的理解(僅供借鑑)

hashcode是一種比equals粗粒度的比較,打個比方,兩個三位數,可以拿十位+個位數作為hashcode,百位+十位+個位才是真正的equals。也就是說,先比較十位+個位數hashcode,如果hashcode不一致,那麼這兩個物件必然不一致,就不用繼續對比了,如果十位+個位數一致,那麼他們有可能是一致的,這時繼續對比equals,才能知道是否兩個物件真的一致。 當然這只是一種粗淺的理解,真正的理解還得看**((h = key.hashCode()) ^ (h >>> 16)) & (n-1)**,直觀上可以理解為就是hash值的補碼取後幾位,問題也不大。

public class InnerClass {
    String a;
    String b;
    String c;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }

        InnerClass that = (InnerClass)o;
        return this.a.equals(that.a);
    }
複製程式碼

如果只是尋求一個解,而不是尋求較優的解的話,那麼很簡單。不是要比equals粗粒度的比較嗎?隨便來一個比equals粗一丁點的就可以了。如下

    @Override
    public int hashCode() {
        return Objects.hash(a);
    }
複製程式碼

這個寫法得到的結果是對的,但是我總覺得不好,因為在我的理解裡,hashcode也是一種效率上的考慮。這樣的話,和直接比較this.a又有多大區別呢?

如果強行要粗粒度,我想了一種方法,例如:

    @Override
    public int hashCode() {
        return Objects.hash(a)/3;
    }
複製程式碼

這樣其實也增加不了效率,因為真正要比較的可以理解為hashcode的後幾位,除不除以3,影響不大。

假設問題變成:對a和b相同的不放入set,那麼hashcode用Object.hash(a)才是一種相對不錯的方式。既滿足了要求,又有一定的效率上的提升。

這麼閉門造車不是個辦法,拜讀一下《effective java》2e.

整理如下:
1.對同一個物件呼叫hashcode時,必須返回同樣的值。
2.equals相同的物件,必須有相同的hashcode。
3.equals不同的物件,建議有不同的hashcode。
4.當不重寫hashcode時,map.put(new A("a"), "b")之後,map.get(new A("a"))不一定能取到"b",因為沒重寫hashcode,put和get時的兩個物件,都是用的Object的hashcode,因為兩次new是兩個不同的物件,所以hashcode不相同,落在不同的bucket。(即便恰好落在相同的bucket,也不一定能獲取到值)
5.hashcode不要返回一個固定值。返回固定值會導致,所有值都落在同一個bucket,這樣程式的時間複雜度會增加。所以最好是讓hashcode均勻的落在不同的bucket。
6.hashcode均勻落在bucket的一個良好實踐是:
----1.一個初始值
----2.對物件的每一個欄位,計算一個值(boolean:f?1:0/byte、char、short、int (int)f/long int(f^(f>>>32))/double Double.doubleToLongBits(f),然後按long處理/物件,對物件的每個欄位呼叫hashcode
----3.result = 31*result + c;
7.對不包含在equals裡對比的欄位,在hashcode中排除掉。
8.對有意義的欄位(即equals裡對比的欄位),不要在hashcode中排除。例如要對a、b做equals,只對a做hashcode,那麼可能hashcode在bucket的分佈就不均勻了,效能就會下降。

根據這些規則,發現以前的很多想法都是錯誤的。正確寫法如下:

@Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + a.hashCode();
        return result;
    }
複製程式碼

要對a、b兩個欄位做聯合去重時:

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + a.hashCode();
        result = 31 * result + b.hashCode();
        return result;
    }
複製程式碼

皮一下:
a做equals,a、b做hashcode,放入hashset結果可能為
"b" "a" "e",
"a" "b" "c",
"b" "c" "e"
這種情況就是違反了第2條,放入了不同的bucket。

之前那個優化的想法不對(ab做equals,a做hash),因為b可能會導致hashcode在bucket的分佈不均勻。

補充:bucket是什麼? bucket就是hashmap中包含一系列entry的東西,每個bucket是一個連結串列或者一顆樹。

TODO:
a.hashcode()
object.hash(a)
object.hashcode(a)
的區別複製程式碼

相關文章