你所不知道的Java之HashCode

以下內(nèi)容為作者辛苦原創(chuàng)辟拷,版權(quán)歸作者所有撞羽,如轉(zhuǎn)載演繹請(qǐng)?jiān)凇肮庾儭蔽⑿殴娞?hào)留言申請(qǐng),轉(zhuǎn)載文章請(qǐng)?jiān)陂_始處顯著標(biāo)明出處衫冻。

之所以寫HashCode诀紊,是因?yàn)槠綍r(shí)我們總聽到它。但你真的了解hashcode嗎隅俘?它會(huì)在哪里使用邻奠?它應(yīng)該怎樣寫?

相信閱讀完本文为居,能讓你看到不一樣的hashcode碌宴。

使用hashcode的目的在于:使用一個(gè)對(duì)象查找另一個(gè)對(duì)象。對(duì)于使用散列的數(shù)據(jù)結(jié)構(gòu)蒙畴,如HashSet贰镣、HashMap、LinkedHashSet膳凝、LinkedHashMap碑隆,如果沒有很好的覆寫鍵的hashcode()和equals()方法,那么將無法正確的處理鍵蹬音。

請(qǐng)對(duì)以下代碼中Person覆寫hashcode()方法上煤,看看會(huì)發(fā)生什么?

// 覆寫hashcode
@Override
public int hashCode() {
    return age;
}

@Test
public void testHashCode() {
    Set<Person> people = new HashSet<Person>();
    Person person = null;
    for (int i = 0; i < 3 ; i++) {
        person = new Person("name-" + i, i);
        people.add(person);
    }
    person.age = 100;
    System.out.println(people.contains(person));
    people.add(person);
    System.out.println(people.size());
}

運(yùn)行結(jié)果并不是預(yù)期的true3著淆,而是false4劫狠!改變person.age后HashSet無法找到person這個(gè)對(duì)象了拴疤,可見覆寫hahcode對(duì)HashSet的存儲(chǔ)和查詢?cè)斐闪擞绊憽?/p>

那么hashcode是如何影響HashSet的存儲(chǔ)和查詢呢?又會(huì)造成怎樣的影響呢嘉熊?

HashSet的內(nèi)部使用HashMap實(shí)現(xiàn)遥赚,所有放入HashSet中的集合元素都會(huì)轉(zhuǎn)為HashMap的key來保存。HashMap使用散列表來存儲(chǔ)阐肤,也就是數(shù)組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)。
存儲(chǔ)結(jié)構(gòu)簡圖如下:

HashMap存儲(chǔ)結(jié)構(gòu)簡圖

數(shù)組的默認(rèn)長度為16讲坎,數(shù)組里每個(gè)元素存儲(chǔ)的是一個(gè)鏈表的頭結(jié)點(diǎn)孕惜。組成鏈表的結(jié)點(diǎn)結(jié)構(gòu)如下:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

每一個(gè)Node都保存了一個(gè)hash----鍵對(duì)象的hashcode,如果鍵沒有按照任何特定順序保存晨炕,查找時(shí)通過equals()逐一與每一個(gè)數(shù)組元素進(jìn)行比較衫画,那么時(shí)間復(fù)雜度為O(n),數(shù)組長度越大瓮栗,效率越低削罩。

所以瓶頸在于鍵的查詢速度,如何通過鍵來快速的定位到存儲(chǔ)位置呢费奸?

HashMap將鍵的hash值與數(shù)組下標(biāo)建立映射弥激,通過鍵對(duì)象的hash函數(shù)生成一個(gè)值,以此作為數(shù)組的下標(biāo)愿阐,這樣我們就可以通過鍵來快速的定位到存儲(chǔ)位置了微服。如果hash函數(shù)設(shè)計(jì)的完美的話,數(shù)組的每個(gè)位置只有較少的值缨历,那么在O(1)的時(shí)間我們就可以找到需要的元素以蕴,從而不需要去遍歷鏈表。這樣就大大提高了查詢速度辛孵。

那么HashMap根據(jù)hashcode是如何得到數(shù)組下標(biāo)呢丛肮?可以拆分為以下幾步:

  • 第一步:h = key.hashCode()
  • 第二步:h ^ (h >>> 16)
  • 第三步:(length - 1) & hash

分析

第一步是得到key的hashcode值;

第二步是將鍵的hashcode的高16位異或低16位(高位運(yùn)算)魄缚,這樣即使數(shù)組table的length比較小的時(shí)候宝与,也能保證高低Bit都參與到Hash的計(jì)算中,同時(shí)不會(huì)有太大的開銷鲜滩;

第三步是hash值和數(shù)組長度進(jìn)行取模運(yùn)算伴鳖,這樣元素的分布相對(duì)來說比較均勻。當(dāng)length總是2的n次方時(shí)徙硅,h & (length-1)運(yùn)算等價(jià)于對(duì)length取模榜聂,這樣模運(yùn)算轉(zhuǎn)化為位移運(yùn)算速度更快。

但是嗓蘑,HashMap默認(rèn)數(shù)組初始化容量大小為16须肆。當(dāng)數(shù)組長度遠(yuǎn)小于鍵的數(shù)量時(shí)匿乃,不同的鍵可能會(huì)產(chǎn)生相同的數(shù)組下標(biāo),也就是發(fā)生了哈希沖突豌汇!

對(duì)于哈希沖突有開放定址法幢炸、鏈地址法、公共溢出區(qū)法等解決方案拒贱。

開放定址法就是一旦發(fā)生沖突宛徊,就尋找下一個(gè)空的散列地址。過程可用下式描述:

fi(key) = (f(key) + di) mod m (di=1,2,3,...,m-1)

例如鍵集合為{12,67,56,16,25,37,22,29,15,47,48,34}逻澳,表長n = 12闸天,取f(key) = key mod 12

前5個(gè)計(jì)算都沒有沖突斜做,直接存入苞氮。如表所示

數(shù)組下標(biāo)
0 12
1 25
2
3
4 16
5
6
7 67
8 56
9
10
11

當(dāng)key = 37時(shí),f(37) = 1瓤逼,與25的位置沖突笼吟。應(yīng)用公式f(37) = (f(37) + 1) mod 12 = 2,所以37存入數(shù)組下標(biāo)為2的位置霸旗。如表所示

數(shù)組下標(biāo)
0 12
1 25
2 37
3
4 16
5
6
7 67
8 56
9
10
11

到了key = 48贷帮,與12所在的0沖突了。繼續(xù)往下找定硝,發(fā)現(xiàn)一直到f(48) = (f(48) + 6) mod 12 = 6時(shí)才有空位皿桑。如表所示

數(shù)組下標(biāo)
0 12
1 25
2 37
3
4 16
5 29
6 48
7 67
8 56
9
10 22
11 47

所以在解決沖突的時(shí)候還會(huì)出現(xiàn)48和37沖突的情況,也就是出現(xiàn)了堆積蔬啡,無論是查找還是存入效率大大降低诲侮。

鏈地址法解決沖突的做法是:如果哈希表空間為[0~m-1],設(shè)置一個(gè)由m個(gè)指針分量組成的一維數(shù)組Array[m], 凡哈希地址為i的數(shù)據(jù)元素都插入到頭指針為Array[i]的鏈表中箱蟆。

它的基本思想是:為每個(gè)Hash值建立一個(gè)單鏈表沟绪,當(dāng)發(fā)生沖突時(shí),將記錄插入到鏈表中空猜。如圖所示:

鏈地址法

鏈表的好處表現(xiàn)在:

  1. remove操作時(shí)效率高绽慈,只維護(hù)指針的變化即可,無需進(jìn)行移位操作
  2. 重新散列時(shí)辈毯,原來散落在同一個(gè)槽中的元素可能會(huì)被散落在不同的地方坝疼,對(duì)于數(shù)組需要進(jìn)行移位操作,而鏈表只需維護(hù)指針谆沃。
    但是钝凶,這也帶來了需要遍歷單鏈表的性能損耗。

公共溢出法就是我們?yōu)樗袥_突的鍵單獨(dú)放一個(gè)公共的溢出區(qū)存放唁影。
例如前面例子中{37,48,34}有沖突耕陷,將他們存入溢出表掂名。如圖所示。

公共溢出法

在查找時(shí)哟沫,先與基本表進(jìn)行比對(duì)饺蔑,如果相等則查找成功,如果不等則在溢出表中進(jìn)行順序查找嗜诀。公共溢出法適用于沖突數(shù)據(jù)很少的情況猾警。

HashMap解決沖突采取的是鏈地址法。整體流程圖(暫不考慮擴(kuò)容)如下:

HashMap存儲(chǔ)流程簡圖

理解了hashcode和哈希沖突即解決方案后裹虫,我們?nèi)绾卧O(shè)計(jì)自己的hashcode()
方法呢肿嘲?

Effective Java一書中對(duì)覆寫hashcode()給出以下指導(dǎo):

  • 給int變量result賦予某個(gè)非零常量值

  • 為對(duì)象內(nèi)每個(gè)有意義的域f計(jì)算一個(gè)int散列碼c

域類型 計(jì)算
boolean c = (f ? 0 : 1)
byte、char筑公、short、int c = (int)f
long c = (int)(f ^ (f >>> 32))
float c = Float.floatToIntBits(f)
double long l = Double.doubleToIntLongBits(f)
c = (int)(l ^ (l >>> 32))
Object c = f.hashcode()
數(shù)組 每個(gè)元素應(yīng)用上述規(guī)則
boolean c = (f ? 0 : 1)
boolean c = (f ? 0 : 1)
  • 合并計(jì)算得到散列碼 result = 37 * result + c

現(xiàn)代IDE通過點(diǎn)擊右鍵上下文菜單可以自動(dòng)生成hashcode方法尊浪,比如通過IDEA生成的hashcode如下:

@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + age;
    return result;
}

但是在企業(yè)級(jí)代碼中匣屡,最好使用第三方庫如Apache commons來生成hashocde方法。使用第三方庫的優(yōu)勢(shì)是可以反復(fù)驗(yàn)證嘗試代碼拇涤。下面代碼顯示了如何使用Apache Commons hash code 為一個(gè)自定義類構(gòu)建生成hashcode捣作。

public int hashCode(){
    HashCodeBuilder builder = new HashCodeBuilder();
    builder.append(mostSignificantMemberVariable);
    ........................
    builder.append(leastSignificantMemberVariable);
    return builder.toHashCode();
}

如代碼所示,最重要的簽名成員變量應(yīng)該首先傳遞然后跟隨的是沒那么重要的成員變量鹅士。

總結(jié)

通過上述分析券躁,我們?cè)O(shè)計(jì)hashcode()應(yīng)該注意的是:

  • 無論何時(shí),對(duì)同一個(gè)對(duì)象調(diào)用hashcode()都應(yīng)該生成同樣的值掉盅。
  • hashcode()盡量使用對(duì)象內(nèi)有意義的識(shí)別信息也拜。
  • 好的hashcode()應(yīng)該產(chǎn)生分布均勻的散列值。

感謝覺醒飛鳥的寶貴建議和辛苦校對(duì)趾痘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末慢哈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子永票,更是在濱河造成了極大的恐慌卵贱,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侣集,死亡現(xiàn)場(chǎng)離奇詭異键俱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)世分,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門编振,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人罚攀,你說我怎么就攤上這事党觅〈瞥危” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵杯瞻,是天一觀的道長镐牺。 經(jīng)常有香客問我,道長魁莉,這世上最難降的妖魔是什么睬涧? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮旗唁,結(jié)果婚禮上畦浓,老公的妹妹穿的比我還像新娘。我一直安慰自己检疫,他們只是感情好讶请,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著屎媳,像睡著了一般夺溢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烛谊,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天风响,我揣著相機(jī)與錄音,去河邊找鬼丹禀。 笑死状勤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的双泪。 我是一名探鬼主播持搜,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼攒读!你這毒婦竟也來了朵诫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤薄扁,失蹤者是張志新(化名)和其女友劉穎剪返,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體邓梅,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡脱盲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了日缨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钱反。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出面哥,到底是詐尸還是另有隱情哎壳,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布尚卫,位于F島的核電站归榕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏吱涉。R本人自食惡果不足惜刹泄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怎爵。 院中可真熱鬧特石,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乞旦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間题山,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工故痊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留顶瞳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓愕秫,卻偏偏與公主長得像慨菱,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子戴甩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容