HashCode算法為什么采用31作為乘數(shù)

一、為什么 String 重寫 equals() 后還需要重寫 hashcode()

hashCode() 的源碼:

兩個對象比較,由于 Java 默認比較的是類的地址值掠兄,每個對象一定是不同的伍掀,所以重寫 hashCode() 和 equals()。重寫 hashCode() 是為了提高效率,因為先根據(jù)類里的屬性生成 hashCode抚恒,如果生成的 hashCode 值不同赫编,則沒必要再使用 equals() 比較屬性的值巡蘸,這樣就大大減少了 equals 的次數(shù),這對大數(shù)據(jù)量比較的效率提高是很明顯的擂送。一個很好的例子就是在集合中的使用:

Java 中的 List 集合是有序的悦荒,因此是可以重復的。set 集合是無序的嘹吨,因此是不能重復的搬味,那么如何保證不能被放入重復的元素呢,單靠 equals() 比較的話蟀拷,如果原來集合中已有 10000 個元素了碰纬,那么放入 10001 個元素,難道要將前面的所有元素都進行比較问芬,看看是否有重復悦析?這個效率可想而知,因此 hashcode 就應遇而生了此衅,Java 就采用了 hash 表强戴,利用哈希算法(也叫散列算法),就是將對象數(shù)據(jù)根據(jù)該對象的特征使用特定的算法將其定義到一個地址上挡鞍,那么在后面定義進來的數(shù)據(jù)只要看對應的 hashCode 地址上是否有值骑歹,那么就用 equals 比較,如果沒有則直接插入墨微,如此大大減少了 equals 的使用次數(shù)道媚,執(zhí)行效率就大大提高了。

為什么必須要重寫 hashCode()翘县,簡言之就是為了保證同一個對象最域,在 equals 相同的情況下 hashCode 值也相同。如果重寫了 equals() 而未重寫 hashCode()炼蹦,可能就會出現(xiàn)兩個沒有關系的對象 equals 相同(因為 equals 都是根據(jù)對象的特征進行重寫的)羡宙,但 hashCode 不同的情況。

String 是遵守類 hashCode 的協(xié)定掐隐,equals() 相同狗热,如何保持 hashCode 相同钞馁。初始 String 對象成員變量 int hash 默認是 0,調用 hashCode() 之后h = 31 * h + val[i]匿刮,其實就是 String 內部根據(jù) char[] 來進行一個運算僧凰。那么只要內容 equals(),其內容運算生產(chǎn)的 hashCode() 也是相等的熟丸。這里也闡明了 equals() 與 hashCode() 在一般應用中的正向關系训措。

二、HashCode算法為什么采用 31 作為乘數(shù)

1??更少的乘積結果沖突
31 是質子數(shù)中一個“不大不小”的存在光羞,如果使用的是一個如 2 的較小質數(shù)绩鸣,那么得出的乘積會在一個很小的范圍,很容易造成哈希值的沖突纱兑。而如果選擇一個 100 以上的質數(shù)呀闻,得出的哈希值會超出 int 的最大范圍,這兩種都不合適潜慎。而如果對超過 50,000 個英文單詞(由兩個不同版本的 Unix 字典合并而成)進行 hashCode() 運算捡多,并使用常數(shù) 31、33铐炫、37垒手、39 和 41 作為乘子,每個常數(shù)算出的哈希值沖突數(shù)都小于 7 個(國外大神做的測試)倒信,那么這幾個數(shù)就被作為生成 hashCode 值的備選乘數(shù)了科贬。

2??31 可以被 JVM 優(yōu)化

位運算是 JVM 里最有效的計算方式:

  1. 【左移 <<】左邊的最高位丟棄,右邊補全0(把 << 左邊的數(shù)據(jù)*2的移動次冪)堤结。
  2. 【右移 >>】把>>左邊的數(shù)據(jù)/2的移動次冪唆迁。
  3. 【無符號右移 >>>】無論最高位是 0 還是 1鸭丛,左邊補齊 0竞穷。

所以:31 * i = (i << 5) - i【例如:31*2=62 轉換為 2*2^5-2=62】

3??理解

  1. 首先 hash 函數(shù)必須要選用質/素數(shù),這個是被科學家論證過的 hash 函數(shù)減少沖突的一個理論鳞溉。
  2. 如果設置為偶數(shù)的話會存在溢出的情況瘾带,導致信息丟失(因為使用偶數(shù)相當于使用了移位運算)。
  3. 可以兼顧到虛擬機的性能熟菲,虛擬機默認使用2<<5-1來得到很好的性能看政,且其是一個不大不小的質數(shù),兼顧了性能和沖突率抄罕。

為什么 31 的性能和解決沖突是最優(yōu)的允蚣?回顧 String 對象的 hashCode():

/**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

選擇不大不小的質數(shù)的原因

正如備注上所描述的那樣,這段代碼的實際執(zhí)行方法如下:

s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1] 推算過程為
i=0 -> h = 31 * 0 + val[0]
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]

因此可以代入式的計算一下呆贿,假如有一個String test = “abcdef”六個字母嚷兔。使用一個較小的質數(shù) 2 或者一個較大的質數(shù) 101 帶入進去森渐,前者2^(6-1) = 32,后者101^(6-1) = 10 510 100 501冒晰。 一個太小一個太大同衣。太小的話 hash 沖突的概率就會相對大一些,太大的話就會太過于分散壶运,對于 hash 結構的集合來說就會占用過的存儲空間耐齐。因此選擇不大不小的質數(shù)是非常有必要的。

如何證明 31 這個數(shù)值就比其他數(shù)值優(yōu)呢蒋情?

整型的數(shù)值區(qū)間是 [-2147483648, 2147483647]埠况,區(qū)間大小為 2^32。所以這里可以將區(qū)間等分成 64 個子區(qū)間棵癣,每個子區(qū)間大小為 2^26询枚。

  1. 乘子 2 算出的哈希值幾乎全部落在第 32 分區(qū),也就是 [0, 67108864) 數(shù)值區(qū)間內浙巫,落在其他區(qū)間內的哈希值數(shù)量幾乎可以忽略不計金蜀。這也就不難解釋為什么數(shù)字 2 作為乘子時,算出哈希值的沖突率如此之高的原因了的畴。

  2. 乘子 101渊抄,沖突率很低,這也說明哈希值溢出并不一定會導致沖突率上升丧裁。但是還是因為乘積太大導致整數(shù)溢出的問題护桦。所以 Java 在設計的時候是為了兼顧性能和 hash 函數(shù)的分散性。

由此煎娇,使用 31 這數(shù)值二庵,可以說是最佳實踐。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末缓呛,一起剝皮案震驚了整個濱河市催享,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哟绊,老刑警劉巖因妙,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異票髓,居然都是意外死亡攀涵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進店門洽沟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來以故,“玉大人,你說我怎么就攤上這事裆操∨辏” “怎么了鳄乏?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長棘利。 經(jīng)常有香客問我橱野,道長,這世上最難降的妖魔是什么善玫? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任水援,我火速辦了婚禮,結果婚禮上茅郎,老公的妹妹穿的比我還像新娘蜗元。我一直安慰自己,他們只是感情好系冗,可當我...
    茶點故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布奕扣。 她就那樣靜靜地躺著,像睡著了一般掌敬。 火紅的嫁衣襯著肌膚如雪惯豆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天奔害,我揣著相機與錄音楷兽,去河邊找鬼。 笑死华临,一個胖子當著我的面吹牛芯杀,可吹牛的內容都是我干的。 我是一名探鬼主播雅潭,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼揭厚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了扶供?” 一聲冷哼從身側響起筛圆,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诚欠,沒想到半個月后顽染,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡轰绵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了尼荆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片左腔。...
    茶點故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖捅儒,靈堂內的尸體忽然破棺而出液样,到底是詐尸還是另有隱情振亮,我是刑警寧澤,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布鞭莽,位于F島的核電站坊秸,受9級特大地震影響,放射性物質發(fā)生泄漏澎怒。R本人自食惡果不足惜褒搔,卻給世界環(huán)境...
    茶點故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望喷面。 院中可真熱鬧星瘾,春花似錦、人聲如沸惧辈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盒齿。三九已至念逞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間边翁,已是汗流浹背肮柜。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留倒彰,地道東北人审洞。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像待讳,于是被迫代替她去往敵國和親芒澜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,566評論 2 349