摘要:的相比的最大的變動就是結構的修改,在之前數組鏈表的基礎上,增加了紅黑樹的結構。如下圖下面我們就來一起學習一下的源碼吧這里我想先吐槽一下的源碼,代碼可讀性比差太多了腦殼疼但是在精簡程度上要比的好一些。
jdk1.8的hashMap相比1.7的最大的變動就是結構的修改,在之前數組+鏈表的基礎上,增加了紅黑樹的結構。
1.7的hashMap我們已經看過了,其中在查找節點的時候,會去根據hash找到對應的數組,接著去遍歷之后的鏈表結構,當hash沖突比較多的時候,鏈表就會非常的長,此時遍歷鏈表的效率就會很低,所以大神們在將紅黑樹加入到了1.8的hashMap中,當鏈表長度大于8的時候,會將鏈表轉換為紅黑樹,提高了查找節點的效率。如下圖:
下面我們就來一起學習一下jdk1.8的hashMap源碼吧!
這里我想先吐槽一下1.8的源碼,代碼可讀性比1.7差太多了...腦殼疼~但是在精簡程度上要比1.7的好一些。
先看put()方法
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) { // 這里p可以視為一個指針,指向tab[i]位置的節點 // n: 數組的length // i: 根據hash算出的數組下標 Node[] tab; Node p; int n, i; // 將tab數組指向table,并判斷table如果為空,則進入resize()中進行初始化 if ((tab = table) == null || (n = tab.length) == 0) // 1.8的hashMap將初始化方法和resize()合并到了一起 n = (tab = resize()).length; // 很據hash值找到tab[i]并將p指向tabl[i],如果沒有內容,創建新的鏈表節點放到i的位置 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, else { // 進入else表示tabl[i]處有內容,下面需要進一步判斷key是否一致 Node e; K k; // 插入的key和tab[i]處的key相等,將p賦值給e(exist)節點 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果該節點是代表紅黑樹的節點,調用紅黑樹的插值方法 else if (p instanceof TreeNode) e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); else { // 進入else說明hash相同,且tab[i]處是一個多節點的鏈表 // 循環鏈表 for (int binCount = 0; ; ++binCount) { // 將e指向p.next,并判斷p.next是否有內容, // 如果沒有內容,說明tab[i]處沒找到一致的key,將會此節點作為新節點插入 // 插入的位置為鏈表尾部 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 鏈表節點數如果大于8,調用treeifyBin將鏈表轉為紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for treeifyBin(tab, hash); break; } // 在tab[i]中找到了相同的key,跳出循環 // 此時e指向tab[i]中key等于新插入key的鏈表節點 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; // 判斷是否需要擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
還需要注意的是
1.8中的put方法是在鏈表結尾插入新節點,而1.7是在頭部插入新節點
1.8是先插入,再擴容,1.7是先擴容,再插入
接下來看一下resize()擴容方法
final Node[] resize() { Node [] oldTab = table; // 當前容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 當前閾值 int oldThr = threshold; int newCap, newThr = 0; // 當前容量>0,表示map中已有內容 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 擴容一倍,并將閾值×2 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 首次put,oldThr > 0 表示使用的是`new HashMap(int initialCapacity)`構造器進行的初始化 else if (oldThr > 0) // initial capacity was placed in threshold // 初始化大小=閾值 newCap = oldThr; // 首次put,else 表示使用的是默認構造器`new HashMap()`進行的初始化 else { // zero initial threshold signifies using defaults // 初始化大小=默認大小(16)并計算閾值 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; // 舊數據遷移 if (oldTab != null) { // 遍歷舊數組 for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // oldTab[j]處只有一個節點,就不需要遍歷鏈表了,直接將此節點賦值到新數組對應hash位置上 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; // e.hash & oldCap 將舊的鏈表分成了lo(e.hash & oldCap為偶數)和hi(e.hash & oldCap為奇數)兩個鏈 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); // lo鏈會分配到和原下標相同的位置 // hi鏈會被分配到原下標+oldCap的位置 if (loTail != null) { loTail.next = null; // lo鏈 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; // hi鏈 newTab[j + oldCap] = hiHead; } } } } } return newTab; }
get() 方法
相對put方法,get就簡單了許多
final NodegetNode(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; }
remove() 刪除節點
final NoderemoveNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node [] tab; Node p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { // node: 要被刪除的節點 Node node = null, e; K k; V v; // 如果頭節點匹配,直接將node指向頭節點 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { // 如果頭節點不匹配,且頭節點屬于紅黑樹節點,從樹中取出要刪除的節點 if (p instanceof TreeNode) node = ((TreeNode )p).getTreeNode(hash, key); // 如果頭節點不匹配,且頭節點屬于鏈表節點,遍歷鏈表取出要刪除的節點 else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //如果找到了符合條件的待刪除節點,根據節點類型去紅黑樹中或者鏈表中刪除指定節點 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode )node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
總結一下:
相對1.7的hashmap不同,1.8的結構采用數組+鏈表+紅黑樹的結構
1.8中的put方法是在鏈表結尾插入新節點,而1.7是在頭部插入新節點
1.8是先插入,再擴容,1.7是先擴容,再插入
至此,1.8的hashMap源碼閱讀到這里就告一段落了,后邊我們會繼續看一下不同版本的ConcurrentHashMap源碼,歡迎觀看~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75466.html
摘要:所謂拉鏈法就是將鏈表和數組相結合。若遇到哈希沖突,則將沖突的值加到鏈表中即可。在編寫程序中,要盡量避免。 目錄: 0-1. 簡介 0-2. 內部結構分析 0-2-1. JDK18之前 0-2-2. JDK18之后 0-3. LinkedList源碼分析 0-3-1. 構造方法 0-3-2. put方法 0-3-3. get方法 0-3-4. resize方法 ...
摘要:值得位數有的次方,如果直接拿散列值作為下標訪問主數組的話,只要算法比較均勻,一般是很難出現碰撞的。但是內存裝不下這么大的數組,所以計算數組下標就采取了一種折中的辦法,就是將得到的散列值與數組長度做一個與操作。 hashMap簡單介紹 hashMap是面試中的高頻考點,或許日常工作中我們只需把hashMap給new出來,調用put和get方法就完了。但是hashMap給我們提供了一個絕佳...
摘要:唐老師,回答道讀源碼是要建立在你的基礎經驗足夠的情況下。除了自己去閱讀源碼之外,比如學習某個類的時候,可以專門結合一些優質的博客針對性的對比學習,并查漏補缺。制定源碼學習計劃。多調試,跟蹤源碼。如若有好的學習方法,可以留言一起交流學習。 序言:目前看一看源碼,來提升自己的技術實力。同時現在好多面試官都喜歡問源碼,問你是否讀過JDK源碼等等? 針對如何閱讀源碼,也請教了我的老師。下面就先...
摘要:注意排版不需要花花綠綠的,盡量使用語法。協議的長連接和短連接,實質上是協議的長連接和短連接。長連接短連接究竟是什么三次握手和四次揮手面試常客為了準確無誤地把數據送達目標處,協議采用了三次握手策略。 一 簡歷該如何寫 1.1 為什么說簡歷很重要?1.2-這3點你必須知道1.3-兩大法則了解一1.4-項目經歷怎么寫?1.5-專業技能該怎么寫?1.6-開源程序員簡歷模板分享1.7 其他的一些...
閱讀 763·2019-08-29 12:49
閱讀 3550·2019-08-29 11:32
閱讀 3434·2019-08-26 10:43
閱讀 2402·2019-08-23 16:53
閱讀 2048·2019-08-23 15:56
閱讀 1695·2019-08-23 12:03
閱讀 2767·2019-08-23 11:25
閱讀 2084·2019-08-22 15:11