HashMap(Java)
何為HashMap:HashMap是常用的數據結構,由數組和鏈表組合構成,可理解為鏈表構成的數組(注:jdk1.8之后如果鏈表長度達到閾值(默認為8),就會更改使用紅黑樹)
1.紅黑樹相關關鍵參數
//一個桶的樹化閾值 //當桶中元素個數超過這個值時,需要使用紅黑樹節點替換鏈表節點 //這個值必須為 8,要不然頻繁轉換效率也不高 static final int TREEIFY_THRESHOLD = 8;想知道為何為8?//一個樹的鏈表還原閾值 //當擴容時,桶中元素個數小于這個值,就會把樹形的桶元素還原為鏈表結構 //這個值應該比上面那個小,至少為 6,避免頻繁轉換 static final int UNTREEIFY_THRESHOLD = 6;//哈希表的最小樹形化容量 //當哈希表中的容量大于這個值時,表中的桶才能進行樹形化 //否則桶內元素太多時會擴容,而不是樹形化 //為了避免進行擴容、樹形化選擇的沖突,這個值不能小于 4 * TREEIFY_THRESHOLD static final int MIN_TREEIFY_CAPACITY = 64;//當多個bin被映射到同一個桶時,如果這個桶中bin的數量小于TREEIFY_THRESHOLD(8)當然不會轉化成樹形結構存儲;如果這個桶中bin的數量大于了 TREEIFY_THRESHOLD(8) ,但是capacity小于MIN_TREEIFY_CAPACITY (64)則依然使用鏈表結構進行存儲,此時會對HashMap進行擴容;如果capacity大于了MIN_TREEIFY_CAPACITY (64),則會進行樹化。這里為何NTREEIFY_THRESHOLD = 6,而TREEIFY_THRESHOLD = 8,也是有依據的。
2.桶的樹形化 treeifyBin()
/*** 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;//若當前哈希表為空,或者數組table的長度小于MIN_TREEIFY_CAPACITY時,進行resize(),//這里需要注意兩點:1.resize()中包括初始賦值,也包括擴容// 2.從源碼中可以看出,hashMap不是有一個鏈表達到TREEIFY_THRESHOLD就會馬上進行紅黑樹轉換,在執行樹形化之前,會先檢查數組長度,如果長度小于64,則對數組進行擴容,而不是進行樹形化。這是一定要注意的if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();//否則進行樹化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);}}TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {return new TreeNode<>(p.hash, p.key, p.value, next);}table發生擴容的時候有兩種情況:
一種是元素達到閥值了,即(Capacity:HashMap當前長度*LoadFactor:負載因子,默認值0.75f)。 一種是HashMap準備樹形化但又發現數組太短(小于MIN_TREEIFY_CAPACITY),這兩種情況均可能發生擴容。這種情況樹形化其實是治標不治本。3.紅黑樹中添加元素 putTreeVal()
在添加時,如果一個桶中已經是紅黑樹結構,就要調用紅黑樹的添加元素方法 putTreeVal()。
4. 紅黑樹中查找元素 getTreeNode()
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value; }5.樹形結構修剪 split()
HashMap 中, resize() 方法的作用就是初始化或者擴容哈希表。當擴容時,如果當前桶中元素結構是紅黑樹,并且元素個數小于鏈表還原閾值 UNTREEIFY_THRESHOLD (默認為 6),就會把桶中的樹形結構縮小或者直接還原(切分)為鏈表結構,調用的就是 split():
6.jdk1.7的頭插法(Entry)和jdk1.8(Node)的尾插法
hashmap在java8之前是頭插法:新來的值會插入到舊值的前面,新值的內存地址成為tableb表的值,原有的值就順推到鏈表中去。java8之后使用的是尾插法:新來的值插到鏈表后面。但是頭插法在并發的情況下,會引發死循環。因為我們都知道,hashmap再達到一定的容量時,會引起HashMap容器的擴容resize(),擴容后就會rehash(),為什么要重新Hash呢?因為長度擴大以后,Hash的規則也隨之改變, hash公式:index = HashCode(Key) & (Length - 1)。而就是再rehash這個過程中,如果有多個并發線程進行rehash,就有可能造成rehash后的鏈表中存在環,(形成環的根本原因:就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置,在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置后,有可能被放到了新數組的不同位置上,如下)
當下一次要在這個鏈表中找一個不存在的值時,就會形成死循環,所以jdk1.8之后就使用了尾插法,但是,即使是這樣,hashmap還是線程不安全的,如果要解決這個問題,考慮ConcurrentHashMap.這篇大佬的文章對JDK1.7Rehash()形成環的過程講得較明白(個人認為)而JDK1.8之后,鏈表有紅黑樹的部分,但鏈表過程時,轉換成紅黑樹,將原本O(n)的時間復雜度降低到了O(logn)。JDK1.8采取的尾插法在擴容時會保持鏈表元素原本的順序,不會出現鏈表成環的問題,原因是擴容轉移后前后鏈表順序不變(JDK1.7擴容轉移后前后鏈表順序倒置,在轉移過程中修改了原來鏈表中節點的引用關系。)
7.hashmap的默認初始化長度:16
默認情況下HashMap的容量是16,但是,如果用戶通過構造函數指定了一個數字作為容量,那么Hash會選擇大于該數字的第一個2的冪作為容量。(3->4、7->8、9->16),這樣做的最終目的就是:只要輸入的HashCode本身分布均勻,Hash算法的結果就是均勻的,可以減少哈希碰撞,實現均勻分布。同時位運算是直接在內存中進行的,效率比取模運算高太多了
原代碼中有如下:
原理:X % 2^n = X & (2^n – 1)
如下例子:
這篇文章寫得太好了
8.JDK1.8 putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判斷當前桶是否為空,空的就需要初始化(resize 中會判斷是否進行初始化)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//根據當前 key 的 hashcode 定位到具體的桶中并判斷是否為空,為空表明沒有 Hash 沖突就直接在當前位置創建一個新桶即可。if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//如果當前桶有值( Hash 沖突),那么就要比較當前桶中的 key、key 的 hashcode 與寫入的 key 是否相等,相等就賦值給 e,在第 8 步的時候會統一進行賦值及返回。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 {//如果是個鏈表,就需要將當前的 key、value 封裝成一個新節點寫入到當前桶的后面(形成鏈表)for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//接著判斷當前鏈表的大小是否大于預設的閾值,大于時就要轉換為紅黑樹。if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//如果在遍歷過程中找到 key 相同時直接退出遍歷。if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//如果 e != null 就相當于存在相同的 key,那就需要將值覆蓋。if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//最后判斷是否需要進行擴容。if (++size > threshold)resize();afterNodeInsertion(evict);return null;}9.JDK1.8 get()方法
public V get(Object key) {Node<K,V> e;//首先將 key hash 之后取得所定位的桶,如果桶為空則直接返回 null 。return (e = getNode(hash(key), key)) == null ? null : e.value;}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) {//否則判斷桶的第一個位置(有可能是鏈表、紅黑樹)的 key 是否為查詢的 key,是就直接返回 value。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<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);}}return null;}8.解決hashmap的線程不安全
使用hashTable或者ConcurrentHashMap,但是因為hashtable并發度的原因基本上沒啥使用場景了(hashtable的解決方法比較粗暴,直接在方法上鎖,并發度很低,最多同時允許一個線程訪問),所以主流都是使用ConcurrentHashMap。
總結
以上是生活随笔為你收集整理的HashMap(Java)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java中equals、==和hashc
- 下一篇: 树节点的遍历,查找,删除(前序,中序,后