摘要:值得位數(shù)有的次方,如果直接拿散列值作為下標(biāo)訪問(wèn)主數(shù)組的話,只要算法比較均勻,一般是很難出現(xiàn)碰撞的。但是內(nèi)存裝不下這么大的數(shù)組,所以計(jì)算數(shù)組下標(biāo)就采取了一種折中的辦法,就是將得到的散列值與數(shù)組長(zhǎng)度做一個(gè)與操作。
hashMap簡(jiǎn)單介紹
hashMap是面試中的高頻考點(diǎn),或許日常工作中我們只需把hashMap給new出來(lái),調(diào)用put和get方法就完了。但是hashMap給我們提供了一個(gè)絕佳的范例,展示了編程中對(duì)數(shù)據(jù)結(jié)構(gòu)和算法的應(yīng)用,例如位運(yùn)算、hash,數(shù)組,鏈表、紅黑樹等,學(xué)習(xí)hashMap絕對(duì)是有好處的。
廢話不多說(shuō),要想學(xué)習(xí)hashMap,必先明白其數(shù)據(jù)結(jié)構(gòu)。在java中,最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)就兩種,一種是數(shù)組,另外一個(gè)就是模擬指針(引用),一起來(lái)看下hashMap結(jié)構(gòu)圖:
從類定義上看,繼承于AbstractMap,并實(shí)現(xiàn)Map接口,其實(shí)就是里面定義了一些常用方法比如size(),isEmpty(),containsKey(),get(),put()等等,Cloneable,Serializable 的作用在之前l(fā)ist章節(jié)已講述過(guò)就不再重復(fù)了,整體來(lái)說(shuō)類定義還是蠻簡(jiǎn)單的
public class HashMap源碼分析extends AbstractMap implements Map , Cloneable, Serializable
接下來(lái)會(huì)帶領(lǐng)大家閱讀源碼,有些不重要的,會(huì)咔掉一部分。
//初始容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默認(rèn)加載因子,用來(lái)計(jì)算threshold static final float DEFAULT_LOAD_FACTOR = 0.75f; //鏈表轉(zhuǎn)成樹的閾值,當(dāng)桶中鏈表長(zhǎng)度大于8時(shí)轉(zhuǎn)成樹 static final int TREEIFY_THRESHOLD = 8; //進(jìn)行resize操作室,若桶中數(shù)量少于6則從樹轉(zhuǎn)成鏈表 static final int UNTREEIFY_THRESHOLD = 6; //當(dāng)桶中的bin樹化的時(shí)候,最小hashtable容量,最少是TREEIFY_THRESHOLD 的4倍 static final int MIN_TREEIFY_CAPACITY = 64;
//在樹化之前,桶中的單個(gè)bin都是node,實(shí)現(xiàn)了Entry接口 static class Node為什么hashMap容量總是2的冪次方?implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } }
//jdk1.8 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hashMap中的hash算法,影響hashMap效率的重要因素之一就是hash算法的好壞。hash算法的好壞,我們可以簡(jiǎn)單的通過(guò)兩個(gè)因素判斷,1是否高效2是否均勻。
大家都知道key.hashCode調(diào)用的是key鍵值類型自帶的哈希函數(shù),返回int散列值。int值得位數(shù)有2的32次方,如果直接拿散列值作為下標(biāo)訪問(wèn)hashMap主數(shù)組的話,只要hash算法比較均勻,一般是很難出現(xiàn)碰撞的。但是內(nèi)存裝不下這么大的數(shù)組,所以計(jì)算數(shù)組下標(biāo)就采取了一種折中的辦法,就是將hash()得到的散列值與數(shù)組長(zhǎng)度做一個(gè)與操作。如下函數(shù):
//或許大家會(huì)發(fā)現(xiàn)這個(gè)方法是jdk1.7,為什么不用1.8的呢?那是因?yàn)?.8里已經(jīng)去掉這個(gè)函數(shù),直接調(diào)用,為了講解方便,我從1.7中找出此方法方便學(xué)習(xí) static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }
hashMap的長(zhǎng)度必須是2的冪次方,最小為16.順便說(shuō)一下這樣設(shè)計(jì)的好處。因?yàn)檫@樣(length-1)正好是一個(gè)高位掩碼,&運(yùn)算會(huì)將高位置0,只保留低位數(shù)字。我們來(lái)舉個(gè)例子,假設(shè)長(zhǎng)度為16,16-1=15,15的2進(jìn)制表示為
00000000 00000000 00000000 00001111.隨意定義一個(gè)散列值做&運(yùn)算,結(jié)果如所示:
10101010 11110011 00111010 01011010 & 00000000 00000000 00000000 00001111 ------------------------------------- 00000000 00000000 00000000 00001010
也就是說(shuō)實(shí)際上只截取了最低的四位,也就是我們計(jì)算的索引結(jié)果。但是只取后幾位的話,就算散列值分布再均勻,hash碰撞也會(huì)很嚴(yán)重,如果hashcode函數(shù)本身不好,分布上成等差數(shù)列的漏洞,使最后幾個(gè)低位成規(guī)律性重復(fù),這就無(wú)比蛋疼了。這時(shí)候hash()函數(shù)的價(jià)值就體現(xiàn)出來(lái)了
h=key.hashcode() 11111011 01011111 00011100 01011011 h>>>16 ^ 00000000 00000000 11111011 01011111 ------------------------------------- 11111011 01011111 11100111 00000100 & 00000000 00000000 00000000 00001111 ------------------------------------- 00000000 00000000 00000000 00000100
(h = key.hashCode()) ^ (h >>> 16),16正好是32的一半,其目的是為了將自己的高半?yún)^(qū)和低半?yún)^(qū)做異或,混合高低位信息,以此來(lái)加大低位的隨機(jī)性,而且混合后的低位存在部分高位的特征,算是變相的保留了高位信息。由此看來(lái)jdk1.8對(duì)于hash算法和計(jì)算索引值的設(shè)計(jì)就基本暴露在我們的眼前了,不得不佩服設(shè)計(jì)之巧妙。
//返回大于等于cap且距離最近的一個(gè)2的冪 //例子:cap=2 return 4; cap=9 return 16; static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
//hashMap數(shù)組,保留著鏈表的頭結(jié)點(diǎn)或者紅黑樹的跟結(jié)點(diǎn)。 //當(dāng)?shù)谝淮问褂玫臅r(shí)候,會(huì)對(duì)其初始化,當(dāng)需要擴(kuò)容時(shí),會(huì)調(diào)用resize方法。長(zhǎng)度一定是2的冪 transient Nodeget方法解析[] table; //用來(lái)遍歷的set集合,速度快于keySet transient Set > entrySet; transient int size; //用來(lái)檢測(cè)使用iterator期間結(jié)構(gòu)是否發(fā)生變化,變化則觸發(fā)fail-fast機(jī)制 transient int modCount; //當(dāng)容器內(nèi)映射數(shù)量達(dá)到時(shí),發(fā)生resize操作(threshold=capacity * load factor) int threshold; //加載因子,默認(rèn)0.75 final float loadFactor;
public V get(Object key) { Nodee; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node getNode(int hash, Object key) { Node [] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode )first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
hash函數(shù)之前已經(jīng)研究過(guò)了,直接鎖定getNode()吧。通過(guò)hash函數(shù)算出hash值&上數(shù)組長(zhǎng)度從而計(jì)算出索引值,然后遍歷比較key,返回對(duì)應(yīng)值。
put和resize方法解析public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { NodeentrySet和keySet[] tab; Node p; int n, i; //若table為空,則調(diào)用resize()進(jìn)行初始化,并將長(zhǎng)度賦值給n if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根據(jù)(n-1)&hash算出索引,得到結(jié)點(diǎn)p,若p為null,則生成一個(gè)新的結(jié)點(diǎn)插入。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //若p不為null,則將p和插入結(jié)點(diǎn)的key與其hash值進(jìn)行比較,若相同將p的引用同時(shí)賦值給e Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若不同,且結(jié)點(diǎn)p屬于樹節(jié)點(diǎn),則調(diào)用putTreeVal() else if (p instanceof TreeNode) e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); else { //若不同,則將p當(dāng)做鏈表的頭結(jié)點(diǎn),循環(huán)比較,若為null則新增節(jié)點(diǎn),且循環(huán)次數(shù)大于等于TREEIFY_THRESHOLD - 1則從鏈表結(jié)構(gòu)轉(zhuǎn)為樹結(jié)構(gòu) 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; } //若鏈表中其中一結(jié)點(diǎn)的key與hash與插入結(jié)點(diǎn)一致,則跳出循環(huán) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果插入的key存在,則替換舊值并返回 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //若插入的key在map中不存在,則判斷size>thresold if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } //初始化數(shù)組和擴(kuò)容使用 final Node [] resize() { Node [] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node [] newTab = (Node [])new Node[newCap]; table = newTab; //若原數(shù)組不為空,則對(duì)其中元素進(jìn)行重新排列 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode )e).split(this, newTab, j, oldCap); else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
hashMap中遍歷的方式是通過(guò)entrySet和keySet,為了保證其效率,建議用entrySet因?yàn)樗拇鎯?chǔ)結(jié)構(gòu)和hashMap一致。hashMap是如何維護(hù)entrySet的呢?通過(guò)閱讀源碼,發(fā)現(xiàn)在put的時(shí)候,并沒(méi)有對(duì)entrySet進(jìn)行維護(hù),且源碼中
entrySet方法只是new了個(gè)對(duì)象,那這個(gè)entrySet視圖的數(shù)據(jù)從哪而來(lái)呢?
public Set> entrySet() { Set > es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; } final class EntrySet extends AbstractSet > { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator > iterator() { return new EntryIterator(); } } final class EntryIterator extends HashIterator implements Iterator > { public final Map.Entry next() { return nextNode(); } } abstract class HashIterator { Node next; // next entry to return Node current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node [] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do {} while (index < t.length && (next = t[index++]) == null); } } final Node nextNode() { Node [] t; Node e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } }
通過(guò)閱讀EntrySet,我們發(fā)現(xiàn)其iterator() 調(diào)用了EntryIterator(),而在對(duì)其進(jìn)行實(shí)例化的時(shí)候會(huì)對(duì)其父類HashIterator進(jìn)行實(shí)例化,從HashIterator的構(gòu)造方法和nextNode我們發(fā)現(xiàn),其返回的視圖就是作用于table的,所以無(wú)需重新開辟內(nèi)存。
總結(jié)本篇文章主要分析hashMap的存儲(chǔ)結(jié)構(gòu),分析了hashMap為什么容量始終是2的冪,分析了其hash算法的好壞和影響其效率的因素,同時(shí)也了解到了在put和get時(shí)做了哪些操作和其中數(shù)據(jù)結(jié)構(gòu)的變化。最后通過(guò)hashMap常見(jiàn)的遍歷方式,得出entrySet是便利效率最高的,且hashMap維護(hù)entrySet的方式。通過(guò)學(xué)習(xí),發(fā)現(xiàn)hashMap的設(shè)計(jì)非常優(yōu)秀,但無(wú)奈能力有限,無(wú)法將其精妙之處全部剖析開來(lái)。
下節(jié)預(yù)告:分析一下并發(fā)下的hashMap有可能造成的閉環(huán)問(wèn)題和concurrentHashMap
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/70746.html
摘要:所謂拉鏈法就是將鏈表和數(shù)組相結(jié)合。若遇到哈希沖突,則將沖突的值加到鏈表中即可。在編寫程序中,要盡量避免。 目錄: 0-1. 簡(jiǎn)介 0-2. 內(nèi)部結(jié)構(gòu)分析 0-2-1. JDK18之前 0-2-2. JDK18之后 0-3. LinkedList源碼分析 0-3-1. 構(gòu)造方法 0-3-2. put方法 0-3-3. get方法 0-3-4. resize方法 ...
摘要:若遇到哈希沖突,則將沖突的值加到鏈表中即可。之后相比于之前的版本,之后在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值默認(rèn)為時(shí),將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時(shí)間。有序,唯一紅黑樹自平衡的排序二叉樹。 本文是最最最常見(jiàn)Java面試題總結(jié)系列第三周的文章。主要內(nèi)容: Arraylist 與 LinkedList 異同 ArrayList 與 Vector 區(qū)別 HashMap的底層...
摘要:下面總結(jié)一下集合常用的三個(gè)子類吧無(wú)序,允許為,底層是散列表紅黑樹,非線程同步有序,不允許為,底層是紅黑樹非線程同步迭代有序,允許為,底層是雙向鏈表,非線程同步從結(jié)論而言我們就可以根據(jù)自己的實(shí)際情況來(lái)使用了。 前言 聲明,本文用的是jdk1.8 前面章節(jié)回顧: Collection總覽 List集合就這么簡(jiǎn)單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡(jiǎn)單【源碼...
摘要:下面我來(lái)簡(jiǎn)單總結(jié)一下的核心要點(diǎn)底層結(jié)構(gòu)是散列表數(shù)組鏈表紅黑樹,這一點(diǎn)和是一樣的。是將所有的方法進(jìn)行同步,效率低下。而作為一個(gè)高并發(fā)的容器,它是通過(guò)部分鎖定算法來(lái)進(jìn)行實(shí)現(xiàn)線程安全的。 前言 聲明,本文用的是jdk1.8 前面章節(jié)回顧: Collection總覽 List集合就這么簡(jiǎn)單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡(jiǎn)單【源碼剖析】 LinkedHas...
閱讀 2400·2021-11-18 10:02
閱讀 3583·2021-11-15 11:36
閱讀 1188·2019-08-30 14:03
閱讀 839·2019-08-30 11:08
閱讀 2829·2019-08-29 13:20
閱讀 3380·2019-08-29 12:34
閱讀 1450·2019-08-28 18:30
閱讀 1704·2019-08-26 13:34