HashMap是如何实现快速存取的
一、存儲(chǔ)實(shí)現(xiàn):put(key,vlaue)
??????首先我們先看源碼:?
| // 將“key-value”添加到HashMap中 ? public?V put(K?key,?V?value) { ? ????// 若“key為null”,則將該鍵值對(duì)添加到table[0]中。 ? ????if?(key?==?null) ? ????????return?putForNullKey(value); ? ????// 若“key不為null”,則計(jì)算該key的哈希值,然后將其添加到該哈希值對(duì)應(yīng)的鏈表中。 ? ????int?hash?= hash(key.hashCode()); ??// 計(jì)算key hash值在table數(shù)組中的位置 ?------------ (1) ????int?i?= indexFor(hash, table.length); ?// 迭代e,從i處開(kāi)始,找到key保存的位置 ?------------ (2) ????for?(Entry<K,V> e?= table[i]; e?!=?null; e?= e.next) { ? ????????Object k; ? ????????// 若“該key”對(duì)應(yīng)的鍵值對(duì)已經(jīng)存在,則用新的value取代舊的value。然后退出! ? ????????if?(e.hash == hash?&& ((k?= e.key) == key?|| key.equals(k))) { ? ????????????V oldValue?= e.value; ? ????????????e.value = value; ? ????????????e.recordAccess(this); ? ????????????return?oldValue; ? ????????} ? ????} ? ?????// 若“該key”對(duì)應(yīng)的鍵值對(duì)不存在,則將“key-value”添加到table中 ? ????modCount++; ????//將key-value添加到table[i]處 ????addEntry(hash, key, value, i); ? ????return?null; ? } |
????? 通過(guò)源碼我們可以清晰看到HashMap保存數(shù)據(jù)的過(guò)程為:首先判斷key是否為null,若為null,則直接調(diào)用putForNullKey方法。若不為空則先計(jì)算key的hash值,然后根據(jù)hash值搜索在table數(shù)組中的索引位置,如果table數(shù)組在該位置處有元素,則通過(guò)比較是否存在相同的key,若存在則覆蓋原來(lái)key的value,否則將該元素保存在鏈頭(最先保存的元素放在鏈尾)。若table在該處沒(méi)有元素,則直接保存。
????? 1、 先看迭代處。此處迭代原因就是為了防止存在相同的key值,若發(fā)現(xiàn)兩個(gè)hash值(key)相同時(shí),HashMap的處理方式是用新value替換舊value,這里并沒(méi)有處理key,這就解釋了HashMap中沒(méi)有兩個(gè)相同的key。
????? 2、 再看(1)、(2)處。這里是HashMap的精華所在。首先是hash方法,該方法為一個(gè)純粹的數(shù)學(xué)計(jì)算,就是計(jì)算h的hash值。
| static?int?hash(int?h) { ? return?useNewHash ? newHash(h) : oldHash(h); ? } |
useNewHash聲明如下:
| private?static?final?boolean?useNewHash; ? static?{?useNewHash?=?false; } |
?
| private?static?int?oldHash(int?h) { ? ????h?+= ~(h?<< 9); ? ????h?^= ?(h?>>> 14); ? ????h?+= ?(h?<< 4); ? ????h?^= ?(h?>>> 10); ? ????return?h; ? } ??? private?static?int?newHash(int?h) { ? ????// This function ensures that hashCodes that differ only by ? ????// constant multiples at each bit position have a bounded ? ????// number of collisions (approximately 8 at default load factor). ? ????h?^= (h?>>> 20) ^ (h?>>> 12); ? ????return?h?^ (h?>>> 7) ^ (h?>>> 4); ? } |
????? 我們知道對(duì)于HashMap的table而言,數(shù)據(jù)分布需要均勻(最好每項(xiàng)都只有一個(gè)元素,這樣就可以直接找到),不能太緊也不能太松,太緊會(huì)導(dǎo)致查詢速度慢,太松則浪費(fèi)空間。計(jì)算hash值后,怎么才能保證table元素分布均與呢?我們會(huì)想到取模,但是由于取模的消耗較大,HashMap是這樣處理的:調(diào)用indexFor方法。
| static?int?indexFor(int?h,?int?length) { ? return?h?& (length-1); ? } |
????? HashMap的底層數(shù)組長(zhǎng)度總是2的n次方,在構(gòu)造函數(shù)中存在:capacity <<= 1;這樣做總是能夠保證HashMap的底層數(shù)組長(zhǎng)度為2的n次方。當(dāng)length為2的n次方時(shí),h&(length - 1)就相當(dāng)于對(duì)length取模,而且速度比直接取模快得多,這是HashMap在速度上的一個(gè)優(yōu)化。至于為什么是2的n次方下面解釋。
????? 我們回到indexFor方法,該方法僅有一條語(yǔ)句:h&(length - 1) ?作用:均勻分布table數(shù)據(jù)和充分利用空間。
????? 這里我們假設(shè)length為16(2^n)和15,h為5、6、7。
| length = 16 | |||
| h | length - 1 | h & length -1 | ? |
| 5 | 15 | 0101 & 1111 = 00101 | 5 |
| 6 | 15 | 0110 & 1111 = 00110 | 6 |
| 7 | 15 | 0111 & 1111 = 00111 | 7 |
| length = 15 | |||
| 5 | 14 | 0101 & 1110 = 00101 | 5 |
| 6 | 14 | 0110 & 1110 = 00110 | 6 |
| 7 | 14 | 0111 & 1110 = 00110 | 6 |
當(dāng)n=15時(shí),6和7的結(jié)果一樣,這樣表示他們?cè)趖able存儲(chǔ)的位置是相同的,也就是產(chǎn)生了碰撞,6、7就會(huì)在一個(gè)位置形成鏈表,這樣就會(huì)導(dǎo)致查詢速度降低。誠(chéng)然這里只分析三個(gè)數(shù)字不是很多,那么我們就看0-15。
| h | length - 1 | h & length - 1 | ? |
| 0 | 14 | 0000 & 1110 = 0000 | 0 |
| 1 | 14 | 0001 & 1110 = 0000 | 0 |
| 2 | 14 | 0010 & 1110 = 0010 | 2 |
| 3 | 14 | 0011 & 1110 = 0010 | 2 |
| 4 | 14 | 0100 & 1110 = 0100 | 4 |
| 5 | 14 | 0101 & 1110 = 0100 | 4 |
| 6 | 14 | 0110 & 1110 = 0110 | 6 |
| 7 | 14 | 0111 & 1110 = 0110 | 6 |
| 8 | 14 | 1000 & 1110 = 1000 | 8 |
| 9 | 14 | 1001 & 1110 = 1000 | 8 |
| 10 | 14 | 1010 & 1110 = 1010 | 10 |
| 11 | 14 | 1011 & 1110 = 1010 | 10 |
| 12 | 14 | 1100 & 1110 = 1100 | 12 |
| 13 | 14 | 1101 & 1110 = 1100 | 12 |
| 14 | 14 | 1110 & 1110 = 1110 | 14 |
| 15 | 14 | 1111 & 1110 = 1110 | 14 |
????? 從上面的圖表中我們看到總共發(fā)生了8此碰撞,同時(shí)發(fā)現(xiàn)浪費(fèi)的空間非常大,有1、3、5、7、9、11、13、15處沒(méi)有記錄,也就是沒(méi)有存放數(shù)據(jù)。這是因?yàn)樗麄冊(cè)谂c14進(jìn)行&運(yùn)算時(shí),得到的結(jié)果最后一位永遠(yuǎn)都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置處是不可能存儲(chǔ)數(shù)據(jù)的,空間減少,進(jìn)一步增加碰撞幾率,這樣就會(huì)導(dǎo)致查詢速度慢。而當(dāng)length = 16時(shí),length – 1 = 15 即1111,那么進(jìn)行低位&運(yùn)算時(shí),值總是與原來(lái)hash值相同,而進(jìn)行高位運(yùn)算時(shí),其值等于其低位值。所以說(shuō)當(dāng)length = 2^n時(shí),不同的hash值發(fā)生碰撞的概率比較小,這樣就會(huì)使得數(shù)據(jù)在table數(shù)組中分布較均勻,查詢速度也較快。
????? 這里我們?cè)賮?lái)復(fù)習(xí)put的流程:當(dāng)我們想一個(gè)HashMap中添加一對(duì)key-value時(shí),系統(tǒng)首先會(huì)計(jì)算key的hash值,然后根據(jù)hash值確認(rèn)在table中存儲(chǔ)的位置。若該位置沒(méi)有元素,則直接插入。否則迭代該處元素鏈表并依此比較其key的hash值。如果兩個(gè)hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),則用新的Entry的value覆蓋原來(lái)節(jié)點(diǎn)的value。如果兩個(gè)hash值相等但key值不等 ,則將該節(jié)點(diǎn)插入該鏈表的鏈頭。具體的實(shí)現(xiàn)過(guò)程見(jiàn)addEntry方法,如下:
| void?addEntry(int?hash, K key, V value,?int?bucketIndex) { ? ????// 獲取bucketIndex處的Entry Entry<K,V> e?= table[bucketIndex]; ? // 將新創(chuàng)建的 Entry 放入 bucketIndex 索引處,并讓新的 Entry 指向原來(lái)的 Entry table[bucketIndex] =?new?Entry<K,V>(hash, key, value, e); ? ????// 若HashMap中元素的個(gè)數(shù)超過(guò)極限了,則容量擴(kuò)大兩倍 if?(size++ >= threshold) ? ????resize(2 * table.length); ? } |
????? 這個(gè)方法中有兩點(diǎn)需要注意:
????? 一、鏈的產(chǎn)生。
系統(tǒng)總是將新的Entry對(duì)象添加到bucketIndex處。如果bucketIndex處已經(jīng)有了對(duì)象,那么新添加的Entry對(duì)象將指向原有的Entry對(duì)象,形成一條Entry鏈,但是若bucketIndex處沒(méi)有Entry對(duì)象,也就是e==null,那么新添加的Entry對(duì)象指向null,也就不會(huì)產(chǎn)生Entry鏈了。
????? 二、擴(kuò)容問(wèn)題。
????? 隨著HashMap中元素的數(shù)量越來(lái)越多,發(fā)生碰撞的概率就越來(lái)越大,所產(chǎn)生的鏈表長(zhǎng)度就會(huì)越來(lái)越長(zhǎng),這樣勢(shì)必會(huì)影響HashMap的速度,為了保證HashMap的效率,系統(tǒng)必須要在某個(gè)臨界點(diǎn)進(jìn)行擴(kuò)容處理。該臨界點(diǎn)在當(dāng)HashMap中元素的數(shù)量等于table數(shù)組長(zhǎng)度*加載因子。但是擴(kuò)容是一個(gè)非常耗時(shí)的過(guò)程,因?yàn)樗枰匦掠?jì)算這些數(shù)據(jù)在新table數(shù)組中的位置并進(jìn)行復(fù)制處理。所以如果我們已經(jīng)預(yù)知HashMap中元素的個(gè)數(shù),那么預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高HashMap的性能。
二、讀取實(shí)現(xiàn):get(key)
????? 相對(duì)于HashMap的存而言,取就顯得比較簡(jiǎn)單了。通過(guò)key的hash值找到在table數(shù)組中的索引處的Entry,然后返回該key對(duì)應(yīng)的value即可。
| // 獲取key對(duì)應(yīng)的value ? public?V get(Object key) { // 若為null,調(diào)用getForNullKey方法返回相對(duì)應(yīng)的value ????if?(key?==?null) ? ???? // 根據(jù)該 key 的 hashCode 值計(jì)算它的 hash 碼 ????????return?getForNullKey(); ? ????// 獲取key的hash值 ? ????int?hash?= hash(key.hashCode()); ? ????// 取出 table 數(shù)組中指定索引處的值,在“該hash值對(duì)應(yīng)的鏈表”上查找“鍵值等于key”的元素 ? ????for?(Entry<K,V> e?= table[indexFor(hash, table.length)]; ? ?????????e?!=?null; ? ?????????e?= e.next) { ? ????????Object k; ? ????????//判斷key是否相同,若key與查找的key相同,則返回相對(duì)應(yīng)的value ????????if?(e.hash == hash?&& ((k?= e.key) == key?|| key.equals(k))) ? ????????????return?e.value; ? ????} ????//沒(méi)找到則返回null ????return?null; ? } |
?????
?在這里能夠根據(jù)key快速的取到value除了和HashMap的數(shù)據(jù)結(jié)構(gòu)密不可分外,還和Entry有莫大的關(guān)系,在前面就提到過(guò),HashMap在存儲(chǔ)過(guò)程中并沒(méi)有將key,value分開(kāi)來(lái)存儲(chǔ),而是當(dāng)做一個(gè)整體key-value來(lái)處理的,這個(gè)整體就是Entry對(duì)象。同時(shí)value也只相當(dāng)于key的附屬而已。在存儲(chǔ)的過(guò)程中,系統(tǒng)根據(jù)key的hashcode來(lái)決定Entry在table數(shù)組中的存儲(chǔ)位置,在取的過(guò)程中同樣根據(jù)key的hashcode取出相對(duì)應(yīng)的Entry對(duì)象。上海尚學(xué)堂java培訓(xùn)原創(chuàng),陸續(xù)java技術(shù)相關(guān)文章奉上,請(qǐng)多關(guān)注。
轉(zhuǎn)載于:https://www.cnblogs.com/shsxt/p/7822868.html
總結(jié)
以上是生活随笔為你收集整理的HashMap是如何实现快速存取的的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Netty(二)——TCP粘包/拆包
- 下一篇: try....exception....