java8中的HashMap原理詳解
java8 HashMap實現(xiàn)原理
HashMap是日常開發(fā)中非常常用的容器,HashMap實現(xiàn)了Map接口,底層的實現(xiàn)原理是哈希表,HashMap不是一個線程安全的容器,jdk8對HashMap做了一些改進,作為開發(fā)人員需要對HashMap的原理有所了解,現(xiàn)在就通過源碼來了解HashMap的實現(xiàn)原理。
首先看HashMap中的屬性
//Node數(shù)組 transient Node<K,V>[] table; //當(dāng)前哈希表中k-v對個數(shù),實際就是node的個數(shù) transient int size; //修改次數(shù) transient int modCount; //元素閾值 int threshold; //負載因子 final float loadFactor;
這里的threshold = loadFactor * table.length,hash表如果想要保持比較好的性能,數(shù)組的長度通常要大于元素個數(shù),默認的負載因子是0.75,用戶可以自行修改,不過最好使用默認的負載因子。
Node是用來存儲KV的節(jié)點,每次put(k,v)的時候就會包裝成一個新的Node, Node定義
static class Node<K,V> implements Map.Entry<K,V> { //hash值 final int hash; final K key; V value; //hash & (capacity - 1) 相同的Node會形成一個鏈表 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } }
put操作
寫入操作是map中最常用的方法,這里看看hashmap的put方法代碼
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
這里先計算key的hash值,然后調(diào)用putVal()方法,其中hash方法是內(nèi)部自帶的一個算法,會對key的hashcode再做一次hash操作
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
pubVal方法
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; //如果數(shù)組為空,先初始化一下 if ((p = tab[i = (n - 1) & hash]) == null) //如果對應(yīng)的數(shù)組為空的話,那么就直接new一個node然后塞進去 tab[i] = newNode(hash, key, value, null); else { //如果有值,說明發(fā)生了沖突,那么就先用拉鏈法來處理沖突 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果頭結(jié)點的key和要插入的key相同,那么就說明找到了之前插入的節(jié)點 else if (p instanceof TreeNode) //如果鏈表轉(zhuǎn)成了紅黑樹 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //如果之前沒有put過這個節(jié)點,那么就new一個新的節(jié)點 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) //另外要檢查一下當(dāng)前鏈表的長度,如果超過8那么就將鏈表轉(zhuǎn)化成紅黑樹 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //如果找到了之前的節(jié)點,那么就跳出 break; p = e; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); //在當(dāng)前類中NOOP return oldValue; } } ++modCount; //如果當(dāng)前元素數(shù)量大于門限值,就要resize整個hash表,實際上就是把數(shù)組擴大一倍,然后將所有元素重新塞到新的hash表中 if (++size > threshold) resize(); afterNodeInsertion(evict); //在該類中NOOP return null; }
在hashtable中默認的出現(xiàn)沖突的時候就會將沖突的元素形成一個鏈表,當(dāng)鏈表長度大于8的時候就會將鏈表變成一個二叉樹,這是java8中做出的改進,因為在使用hash表的時候在key特殊的情況下最壞的時候hash表會退化成一個鏈表,那么原有的O(1)的時間復(fù)雜度就變成了O(n),性能就會大打折扣,但是引用了紅黑樹之后那么在最好的情況下時間復(fù)雜度就變成了O(log(n))。
resize方法
final Node<K, V> [] resize() { ...... //去掉了一些代碼,只關(guān)注最核心的node遷移 //resize會新建一個數(shù)組,數(shù)組的長度是原來數(shù)組長度的兩倍 for (int j = 0; j < oldCap; ++j) {//遍歷原來的數(shù)組 Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //如果沒有形成鏈表的話,就直接塞到新的hash表中 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //紅黑樹操作?? else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { //如果hash值小于oldCap的時候,那么就還在原來那個數(shù)組的位置,就把這個節(jié)點放到low鏈表中 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { //否則的話就是因為擴展數(shù)組長度,就把原來的節(jié)點放到high鏈表中 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; //low鏈表還放在原來的位置 } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; //high鏈表放到j(luò)+oldCap位置上 } } } } }
resize操作就是創(chuàng)建一個先的數(shù)組,然后把老的數(shù)組中的元素塞到新的數(shù)組中,注意java8中的hashMap中數(shù)組長度都是2的n次冪,2、4、、8、16….. 這樣的好處就是可以通過與操作來替代求余操作。當(dāng)數(shù)組擴大之后,那么每個元素所在的位置是可以預(yù)期的,就是要不就待在原來的位置,要不就是到j(luò)+oldCap位置上,舉個栗子,如果原來數(shù)組長度為4,那么hash為3和7 的元素都會放在index為3的位置上,當(dāng)數(shù)組長度變成8的時候,hash為3的元素還待在index為3的位置,hash為7的元素此時就要放到index為7的位置上。
resize操作是一個很重要的操作,resize會很消耗性能,因此在創(chuàng)建hashMap的時候最好先預(yù)估容量,防止重復(fù)創(chuàng)建拷貝。
另外hashmap也是非線程安全的,在多線程操作的時候可能會產(chǎn)生cpu100%的情況,主要的原因也是因為在多個線程resize的時候?qū)е骆湵懋a(chǎn)生了環(huán),這樣下次get操作的時候就會容易進入死循環(huán)。
get方法()
get的實現(xiàn)比較簡單
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) //如果節(jié)點不為空而且頭結(jié)點與查找的key相同就返回 return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)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); //遍歷鏈表查找key相同的node } } return null; }
到此這篇關(guān)于java8中的HashMap原理詳解的文章就介紹到這了,更多相關(guān)HashMap原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解spring cloud ouath2中的資源服務(wù)器
這篇文章主要介紹了spring cloud ouath2中的資源服務(wù)器的相關(guān)知識,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02springboot如何讀取配置文件(application.yml)中的屬性值
本篇文章主要介紹了springboot如何讀取配置文件(application.yml)中的屬性值,具有一定的參考價值,有興趣的小伙伴可以了解一下2017-04-04Java實現(xiàn)順序表和鏈表結(jié)構(gòu)
大家好,本篇文章主要講的是Java實現(xiàn)順序表和鏈表結(jié)構(gòu),感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下2022-02-02SpringBoot整合MybatisSQL過濾@Intercepts的實現(xiàn)
這篇文章主要介紹了SpringBoot整合MybatisSQL過濾@Intercepts的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03