您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關(guān)如何調(diào)用HashMap原理以及put方法和get方法,小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
HashMap的數(shù)據(jù)結(jié)構(gòu)為數(shù)組+鏈表,以key,value的形式存值,通過調(diào)用put與get方法來存值與取值。
它內(nèi)部維護(hù)了一個(gè)Entry數(shù)組,得到key的hashCode值將其移位按位與運(yùn)算,然后再通過跟數(shù)組的長(zhǎng)度-1作邏輯與運(yùn)算得到一個(gè)index值來確定數(shù)據(jù)存儲(chǔ)在Entry數(shù)組當(dāng)中的位置,通過鏈表來解決hash沖突問題。
當(dāng)發(fā)生碰撞了,對(duì)象將會(huì)儲(chǔ)存在鏈表的下一個(gè)節(jié)點(diǎn)中。
接觸過HashMap的小伙伴都會(huì)經(jīng)常使用put和get這些方法,那接下來就對(duì)HashMap的內(nèi)部存儲(chǔ)進(jìn)行詳解.(以初學(xué)者的角度進(jìn)行分析)-(小白篇)
當(dāng)程序試圖將多個(gè) key-value 放入 HashMap 中時(shí),以如下代 碼片段為例:
上面代碼,創(chuàng)建了一個(gè)HashMap對(duì)象,并且指定了容量(capacity)和負(fù)載因子(loadFactor),然后put,以鍵值對(duì)的方式儲(chǔ)存值. 容量咱們很容易理解(默認(rèn)16容量),也就是給它一個(gè)初始化的長(zhǎng)度,那么負(fù)載因子又是個(gè)啥?
負(fù)載因子 : 表示HashMap滿的程度,默認(rèn)值為0.75f,也就是說默認(rèn)情況下,當(dāng)HashMap中元素個(gè)數(shù)達(dá)到了容量的3/4的時(shí)候就會(huì)進(jìn)行自動(dòng)擴(kuò)容.(這里我把負(fù)載因子設(shè)置到0.9f,這么做的原因是想讓"效果"更明顯,啥效果,后面講解.) 具體擴(kuò)容多少,源碼有這樣一段代碼如下:
我們從這里可以知道閾(yu)值的計(jì)算公式:
閾值(threshold) = 負(fù)載因子(loadFactor) * 容量(capacity)
來,上源碼 如下:
這是源碼的構(gòu)造函數(shù),來看看最后一行代碼用 tableSizeFor(initialCapacity) 方法來計(jì)算出閾值,
查看此方法源碼 如下:
cap
參數(shù)也就是給的初始容量,這段算法會(huì)給出一個(gè)距離參數(shù)cap 最近的并且沒有變小的 2 的冪次方數(shù),比如傳入10 返回 16,就是這么神奇!
以上我們了解了HashMap的擴(kuò)容機(jī)制,也知道了創(chuàng)建一個(gè)HashMap對(duì)象的內(nèi)部活動(dòng). 下面我們對(duì)put添加一個(gè)鍵值對(duì)的方法進(jìn)行解析.
我們知道HashMap是以key-value的形式保存的,取用get()方法查找key來獲取相對(duì)應(yīng)的value. 我們可以調(diào)試put值時(shí)看出HashMap底層是用數(shù)組構(gòu)成的,并且存放的位置是散列無序的,這點(diǎn)不像數(shù)組按存放的先后順序來排列.如下圖:
當(dāng)put完第4個(gè)值時(shí)發(fā)現(xiàn)只顯示了3個(gè)元素,之后一個(gè)個(gè)點(diǎn)開元素后發(fā)現(xiàn),第4個(gè)元素出現(xiàn)在next這個(gè)屬性中. 如下圖:
然后繼續(xù)put完全部值,在看,一共存放了12個(gè)值,但是table中只有9個(gè)元素,還發(fā)現(xiàn)閾(yu)[ threshold ]值從最初的7增加到了15,容量(capacity)也從原來給的8變成了16,說明觸發(fā)了擴(kuò)容機(jī)制(從源代碼可看到容量擴(kuò)充至原來的二倍),在一個(gè)我們剛剛發(fā)現(xiàn)了有些值跑到了另一些值的next屬性里去了.我們點(diǎn)開元素的next屬性看看,是不是跑到這里頭了.如下圖:
果然,這三倆跑人家的底盤來了.在下標(biāo) 7,13,14中的Next的屬性中找到了"遺失"的三個(gè)元素.
在看如下圖:
仔細(xì)瞅瞅,發(fā)現(xiàn)每個(gè)元素都有這么一個(gè)next的屬性,有些為空,有些不為空,不為空的則是元素存放在此next中,有沒有感覺元素被next屬性組成了一條鏈子.來上圖(形象又生動(dòng)):
此圖模擬了內(nèi)部的結(jié)構(gòu)方式,在同一下標(biāo)中同時(shí)存在多個(gè)元素,產(chǎn)生了鏈表結(jié)構(gòu)圖中的箭頭也就表示著每個(gè)元素中的next屬性,看到這會(huì)發(fā)現(xiàn)許多詭異所思的問題, 為啥它存儲(chǔ)是無序的呢? , 為啥存著存著都跑到一塊去了,成了鏈表結(jié)構(gòu)呢?,等一些問題.咱們下面通過源碼來看看.(源碼如下):
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
既然叫HashMap,當(dāng)然得和Hash扯點(diǎn)關(guān)系啦.
HashMap 采用一種所謂的“Hash 算法”來決定每個(gè)元素的存儲(chǔ)位置。當(dāng)程序執(zhí)行 map.put(String,Obect)方法 時(shí),系統(tǒng)將調(diào)用String的 hashCode() 方法得到其 hashCode 值——每個(gè) Java 對(duì)象都有 hashCode() 方法,都可通過該方法獲得它的 hashCode 值。得到這個(gè)對(duì)象的 hashCode 值之后,系統(tǒng)會(huì)根據(jù)該 hashCode 值來決定該元素的存儲(chǔ)位置。小伙伴可以試試調(diào)用hashCode()方法看看經(jīng)過此算法會(huì)得出怎樣的結(jié)果.
咱們現(xiàn)在知道為啥是無序存放的了,key通過哈希算法的值來決定它存儲(chǔ)的位置,那出現(xiàn)的重疊現(xiàn)象表明,不同的key經(jīng)過哈希算法得出的值會(huì)出現(xiàn)相等的可能(這樣的現(xiàn)狀稱為碰撞/沖突),所以一個(gè)下標(biāo)會(huì)出現(xiàn)多個(gè)元素,形成鏈表結(jié)構(gòu).至于為什么采用鏈表,是為了節(jié)省空間,鏈表在內(nèi)存中并不是連續(xù)存儲(chǔ),所以我們可以更充分地使用內(nèi)存。
(下面我們將每個(gè)下標(biāo)統(tǒng)稱為Entry(桶),也就是一個(gè) key-value 對(duì))
有沒有覺得這樣會(huì)降低查詢的效率(鏈表),進(jìn)行查詢時(shí),先查找到Entry,在通過鏈的遍歷.想著都覺得麻煩,雖然這樣解決了碰撞這樣的沖突,但是引來了一個(gè)大毛病(查找效率降低),這得不行啊,人家HashMap同志就是以快出名啊,所以在jdk8中進(jìn)行了優(yōu)化, 引入了樹結(jié)構(gòu),在鏈表長(zhǎng)度大于8的時(shí)候,將后面的數(shù)據(jù)存在紅黑樹中,以加快檢索速度,,來優(yōu)化 鏈 過長(zhǎng)所帶來的性能低化的問題.
來上碼,繼續(xù)查看putVal(hash(key), key, value, false, true); 的源碼:
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
咱們看完注釋應(yīng)該了解它的大概了,繼續(xù)查看treeifyBin()將鏈表改為紅黑樹 (jdk8新特性)方法碼:
/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 如果數(shù)組等于null 或 數(shù)組長(zhǎng)度小于 64 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 重新散列,使得鏈表變短 resize(); // 如果hash沖突,且數(shù)組長(zhǎng)度大于 64,則只能使用紅黑樹結(jié)構(gòu) else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; // 返回新的紅黑樹 do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
以上介紹了MashMap對(duì)存儲(chǔ)數(shù)據(jù)的機(jī)制進(jìn)行簡(jiǎn)短的介紹.我們已經(jīng)知道產(chǎn)生碰撞會(huì)導(dǎo)致查詢效率打折扣,那么如何能有效的避免哈希碰撞呢?
咱們先反向思維一下,你認(rèn)為什么情況會(huì)導(dǎo)致HashMap的哈希碰撞比較多?
無外乎兩種情況:
1、容量太小。容量小,碰撞的概率就高了。狼多肉少,就會(huì)發(fā)生爭(zhēng)強(qiáng)。
2、hash算法不夠好。算法不合理,就可能都分到同一個(gè)或幾個(gè)桶中。分配不均,也會(huì)發(fā)生爭(zhēng)搶。
所以,解決HashMap中的哈希碰撞也是從這兩方面入手。
這兩點(diǎn)在HashMap中都有很好的提現(xiàn)。兩種方法相結(jié)合,在合適的時(shí)候擴(kuò)大數(shù)組容量,再通過一個(gè)合適的hash算法計(jì)算元素分配到哪個(gè)數(shù)組中,就可以大大的減少?zèng)_突的概率。但數(shù)據(jù)量大時(shí),碰撞也會(huì)成正比的增長(zhǎng),所以引入紅黑樹的結(jié)構(gòu),就能避免查詢效率低下的問題。
咱們?cè)賮砜纯簇?fù)載因子這個(gè)影響性能的平衡點(diǎn)有啥規(guī)律.上文已經(jīng)對(duì)啥是負(fù)載因子進(jìn)行了解釋.
它Hsah表中元素的填滿的程度.
若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:沖突的機(jī)會(huì)加大了.鏈表長(zhǎng)度會(huì)越來越長(zhǎng),查找效率降低。
反之,加載因子越小,填滿的元素越少,好處是:沖突的機(jī)會(huì)減小了,但:空間浪費(fèi)多了.表中的數(shù)據(jù)將過于稀疏(很多空間還沒用,就開始擴(kuò)容了)
沖突的機(jī)會(huì)越大,則查找的成本越高.
因此,必須在 "沖突的機(jī)會(huì)"與"空間利用率"之間尋找一種平衡與折衷. 這種平衡與折衷本質(zhì)上是數(shù)據(jù)結(jié)構(gòu)中有名的"時(shí)-空"矛盾的平衡與折衷.
這里寫了段測(cè)試代碼 如下:
public class HashTest { public static void main(String[] args) { // 對(duì)"負(fù)載因子的大小對(duì)程序的影響規(guī)律"進(jìn)行測(cè)試 // threshold=capacity * loadFactor ---- 閾值 = 容量 x 負(fù)載因子 // 源代碼擴(kuò)容后容量是擴(kuò)容前的二倍 int n1 = 10; // 對(duì)照組 int n2 = 1000000; // put/get多少組 long t0 = 0; //總耗時(shí) float lf = 0.9f; //負(fù)載因子 int capacity = 100; //初始容量 HashMap map = null; //對(duì)照組循環(huán) for (int j = 1; j <= n1; j++) { map = new HashMap(capacity, lf); List<String> list = new ArrayList<String>(); // 利用循環(huán)進(jìn)行put for (int i = 0; i < n2; i++) { String temp = HashTest.randomString(); map.put(temp, i); list.add(temp); } long time = 0; // 總耗費(fèi)時(shí)間 // 利用循環(huán)get for (int i = 0; i < n2; i++) { String temp = list.get(i); long t1 = System.currentTimeMillis(); map.get(temp); long t2 = System.currentTimeMillis(); long t3 = t2 - t1;// 花費(fèi)時(shí)間 time += t3; } System.out.println("組"+j+"花費(fèi)時(shí)間(ms)=" + time); t0 += time; map = null; } System.out.println("get出 "+n2+" 對(duì)鍵值對(duì)中,"+n1+"組數(shù)據(jù)得出:"); System.out.println("---------------------------------"); System.out.println("每get"+n2+"對(duì)鍵值對(duì) 平均花費(fèi)時(shí)間(毫秒):"+(t0/n1)); } /** * 產(chǎn)生隨機(jī)字符串方法 * @return */ public static String randomString() { // 最終產(chǎn)生的字符串 StringBuffer sb = new StringBuffer(); // 字符串樣本 String str = "回到家卡薩恒大帝景阿薩德節(jié)快樂就看見了困窘企業(yè)無辜的鄙視你別這么想按一個(gè)預(yù)告的哈上東國際按時(shí)大大伽伽匯頂科技啊啥看的撒打算大的歐亞報(bào)出去qwertyuiopasdfghjklzxvcbnm,.;p[']/\1234567890zxcvbnmaksjhfgdlpoiuytrewq阿斯加德克拉斯近段時(shí)間的書上方法更符合輔導(dǎo)費(fèi)的冠福股份極樂空間流口水"; // System.out.println("樣本字符串長(zhǎng)度:"+str.length()); // 產(chǎn)生一個(gè)1到30的數(shù)字 int num = (int) (Math.random() * 30 + 1); // System.out.println("num="+num); // 用for循環(huán)從樣本字符串中提取出字符進(jìn)行組合 for (int i = 0; i < num; i++) { int num1 = (int) (Math.random() * str.length()); // 產(chǎn)生一個(gè)0到字符串樣本的數(shù)字 // 根據(jù)索引值獲取對(duì)應(yīng)的字符 char charAt = str.charAt(num1); sb.append(charAt); } // System.out.println("產(chǎn)生一個(gè)長(zhǎng)度為"+num+"的字符串"); return sb.toString(); }
小伙伴可以調(diào)節(jié)負(fù)載因子的大小來測(cè)試,時(shí)間上的差異.
以上就是如何調(diào)用HashMap原理以及put方法和get方法,小編相信有部分知識(shí)點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見到或用到的。希望你能通過這篇文章學(xué)到更多知識(shí)。更多詳情敬請(qǐng)關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。