摘要:當容量超過容量負載因子時,進行擴容操作確定何時將沖突的鏈表轉換成紅黑樹用來確何時將紅黑樹轉換成鏈表當鏈表轉換成紅黑樹時,需要判斷數組容量。桶排序核心思想是根據數據規模劃分,個相同大小的區間每個區間為一個桶,桶可理解為容器。
剛剛看到QQ群有人吹Hashmap,一想我啥都不懂,就趕快補了一波。下面分享一下我對Hashmap的理解,主要用于個人備忘。如果有不對,請批評。想要解鎖更多新姿勢?請訪問http://blog.tengshe789.tech/
總起
Hashmap是散列表,存儲結構是鍵值對形式。根據健的Hashcode值存儲數據,有較快的訪問速度。
它的線程是不安全的,在兩個線程同時嘗試擴容HashMap時,可能將一個鏈表形成環形的鏈表,所有的next都不為空,進入死循環;要想讓它安全,可以用 Collections的synchronizedMap 方法使 HashMap具有線程安全的能力,或者使用ConcurrentHashMap 。
他的鍵值對都可以為空,映射不是有序的。
Hashmap有兩個參數影響性能:初始容量,加載因子。
Hashmap存儲結構
JDK1.8中Hashmap是由鏈表、紅黑樹、數組實現的
//用來實現數組、鏈表的數據結構 static class Nodeimplements Map.Entry { final int hash;//保存節點的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 K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry,?> e = (Map.Entry,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
Hashmap構造方法
HashMap有4個構造方法。
代碼:
//方法1.制定初始容量和負載因子 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } //方法2.指定初始容量 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //方法三。無參構造。 HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //方法四。將另一個 Map 中的映射拷貝一份到自己的存儲結構中來,這個方法不是很常用 public HashMap(Map extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
Hashmap變量成員
//未指定容量的時候,數組的初始容量。初始容量是16 //為什么不直接寫16?因為速度快。計算機里面要轉換二進制。 //必須2的n次冪 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //負載因子。當hashmap容量超過 容量*負載因子 時,進行擴容操作(resize()) static final float DEFAULT_LOAD_FACTOR = 0.75f; //確定何時將hash沖突的鏈表轉換成紅黑樹 static final int TREEIFY_THRESHOLD = 8; //用來確何時將紅黑樹轉換成鏈表 static final int UNTREEIFY_THRESHOLD = 6; //當鏈表轉換成紅黑樹時,需要判斷數組容量。若數組容量太小導致hash沖突太多,則不進行紅黑樹操作,轉而利用reseize擴容 static final int MIN_TREEIFY_CAPACITY = 64;
初始容量、負載因子、閾值.
一般情況下,使用無參構造方法創建 HashMap。但當我們對時間和空間復雜度有要求的時候,使用默認值有時可能達不到我們的要求,這個時候我們就需要手動調參。
在 HashMap 構造方法中,可供我們調整的參數有兩個,一個是初始容量initialCapacity,另一個負載因子loadFactor。通過這兩個設定這兩個參數,可以進一步影響閾值大小。但初始閾值 threshold 僅由initialCapacity 經過移位操作計算得出。
名稱 用途
initialCapacity HashMap 初始容量
loadFactor 負載因子
threshold 當前 HashMap 所能容納鍵值對數量的最大值,超過這個值,則需擴容
默認情況下,HashMap 初始容量是16,負載因子為 0.75。 注釋中有說明,閾值可由容量乘上負載因子計算而來 ,即threshold = capacity * loadFactor
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; }
這段代碼有點難,根據大神的說法,這個方法的意思是,找到大于或等于 cap 的最小2的冪。我們先來看看 tableSizeFor 方法的圖解 :
圖中容量是229+1,計算后是230
引用一下啊大神說的:
對于 HashMap 來說,負載因子是一個很重要的參數,該參數反應了 HashMap 桶數組的使用情況(假設鍵值對節點均勻分布在桶數組中)。通過調節負載因子,可使 HashMap 時間和空間復雜度上有不同的表現。當我們調低負載因子時,HashMap 所能容納的鍵值對數量變少。擴容時,重新將鍵值對存儲新的桶數組里,鍵的鍵之間產生的碰撞會下降,鏈表長度變短。此時,HashMap 的增刪改查等操作的效率將會變高,這里是典型的拿空間換時間。相反,如果增加負載因子(負載因子可以大于1),HashMap 所能容納的鍵值對數量變多,空間利用率高,但碰撞率也高。這意味著鏈表長度變長,效率也隨之降低,這種情況是拿時間換空間。至于負載因子怎么調節,這個看使用場景了。一般情況下,我們用默認值就可以了。
插入PUT
過程:
對Key求hash值,然后計算下標
如果沒有碰撞,就放入桶中
如果碰撞了,就以鏈表形式放到后面
如果鏈表長度超過閾值,就把鏈表轉換成紅黑樹
如果鏈表存在則替換舊值
如果桶滿了(容量*負載因子),則重新resize
public V put(K key, V value) {
//調用核心方法 return putVal(hash(key), key, value, false, true); }
putVal
核心算法在putVal()中。要想理解,先要明白桶排序(Bucket Sort)
它是迄今為止最快的一種排序法,其時間復雜度僅為Ο(n),也就是線性復雜度。
桶排序核心思想是:根據數據規模n劃分,m個相同大小的區間 (每個區間為一個桶,桶可理解為容器) 。將n個元素按照規定范圍分布到各個桶中去 ,再對每個桶中的元素進行排序,排序方法可根據需要,選擇快速排序,或者歸并排序,或者插入排序 ,然后依次從每個桶中取出元素,按順序放入到最初的輸出序列中(相當于把所有的桶中的元素合并到一起) 。
下面是代碼:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //n是數組長度 Node[] tab; Node p; int n, i; //判斷桶數組是否是空 if ((tab = table) == null || (n = tab.length) == 0) //是就用resize()初始化 n = (tab = resize()).length; //根據 hash 值確定節點在數組中的插入位置 //若此位置沒有元素則進行插入,注意確定插入位置所用的計算方法為 (n - 1) & hash,由于 n 一定是2的冪次,這個操作相當于hash % n if ((p = tab[i = (n - 1) & hash]) == null) //將新節點引入桶中 tab[i] = newNode(hash, key, value, null); else { //臨時變量e進行記錄。如果有值,說明僅僅是值的覆蓋。 Node e; K k; // 如果鍵的值以及節點 hash 等于鏈表中的第一個鍵值對節點時,則將 e 指向該鍵值對 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode)// 如果桶中的引用類型為 TreeNode,則調用紅黑樹的插入方法 e = ((TreeNode )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; } } //臨時變量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; }
HASH
hash算法,高十六位與低十六進行異或運算,這樣做的好處是使得到結果會盡可能不同。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
resize
HashMap 的擴容機制與其他變長集合的套路不太一樣,HashMap 按當前桶數組長度的2倍進行擴容,閾值也變為原來的2倍(如果計算過程中,閾值溢出歸零,則按閾值公式重新計算)。擴容之后,要重新計算鍵值對的位置,并把它們移動到合適的位置上去。
resize總共做了3件事,分別是:
計算新桶數組的容量 newCap 和新閾值 newThr
根據計算出的 newCap 創建新的桶數組,桶數組 table 也是在這里進行初始化的
將鍵值對節點重新映射到新的桶數組里。如果節點是 TreeNode 類型,則需要拆分紅黑樹。如果是普通節點,則節點按原順序進行分組。
//resize()函數在size > threshold時被調用
final Node
Node[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //oldCap大于 0 代表原來的 table 非空 if (oldCap > 0) { // 當 table 容量超過容量最大值,則不再擴容 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 } else if (oldThr > 0) // initial capacity was placed in threshold /* *oldCap 小于等于 0 且 oldThr 大于0,代表用戶創建了一個 HashMap,但是使用的構造函數為 * HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity) * 或 HashMap(Map extends K, ? extends V> m),導致 oldTab 為 null,oldCap 為0, * oldThr 為用戶指定的 HashMap的初始容量 */ newCap = oldThr; else { //設置新容量和新閾值大小 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // newThr 為 0 時,按閾值計算公式進行計算 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) { //遍歷。把 oldTab 中的節點 reHash 到 newTab 中去 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; //若節點是 TreeNode 節點,要進行 紅黑樹的 rehash 操作 else if (e instanceof TreeNode) ((TreeNode )e).split(this, newTab, j, oldCap); //若是鏈表,進行鏈表的 rehash 操作 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; // rehash 后節點新的位置一定為原來基礎上加上 oldCap newTab[j + oldCap] = hiHead; } } } } } return newTab;
}
關于HashMap在什么時候時間復雜度是O(1),什么時候是O(n),什么時候又是O(logn)的問題
O(1):鏈表的長度盡可能短,理想狀態下鏈表長度都為1
O(n):當 Hash 沖突嚴重時,如果沒有紅黑樹,那么在桶上形成的鏈表會變的越來越長,這樣在查詢時的效率就會越來越低;時間復雜度為O(N)。
O(logn):采用紅黑樹之后可以保證查詢效率O(logn)
手寫
/** * @author tengshe789 */ public class 手寫HashMap { public static class Node{ K key; V value; Node next; public Node(K key, V value, Node next) { this.key = key; this.value = value; this.next = next; } public K getKey() { return this.key; } public V getValue() { return this.value; } public V setValue(V value) { this.value=value; return this.value; } } public static class HashMap { /*數據存儲的結構==>數組+鏈表*/ Node [] array=null; /* 哈希桶的長度 */ private static int defaultLength=16; /*加載因子/擴容因子*/ private static double factor=0.75D; /*集合中的元素個數*/ private int size; /*打印函數*/ public void print() { System.out.println("==============================="); if(array!=null) { Node node=null; for (int i = 0; i < array.length; i++) { node=array[i]; System.out.print("下標["+i+"]"); //遍歷鏈表 while(node!=null) { System.out.print("["+node.getKey()+":"+node.getValue()+"]"); if(node.next!=null) { node=node.next; }else { //到尾部元素 node=null; } } System.out.println(); } } } //put元素方法 public V put(K k, V v) { //1.懶加載機制,使用的時候進行分配 if(array==null) { array=new Node[defaultLength]; } //2.通過hash算法,計算出具體插入的位置 int index=position(k,defaultLength); //擴容。判斷是否需要擴容 //擴容的準則,元素的個數 大于 桶的尺寸*加載因子 if(size > defaultLength*factor) { resize(); } //3.放入要插入的元素 Node node=array[index]; if(node==null) { array[index]=new Node (k,v,null); size++; }else { if(k.equals(node.getKey()) || k==node.getKey()) { return node.setValue(v); }else { array[index]=new Node (k,v,node); size++; } } return null; } //擴容,并且重新排列元素 private void resize() { //翻倍擴容 //1.創建新的array臨時變量,相當于defaultlength*2 Node [] temp=new Node[defaultLength << 1]; //2.重新計算散列值,插入到新的array中去。 code=key % defaultLength ==> code=key % defaultLength*2 Node node=null; for (int i = 0; i < array.length; i++) { node=array[i]; while(node!=null) { //重新散列 int index=position(node.getKey(),temp.length); //插入頭部 Node next = node.next; //3 node.next=temp[index]; //1 temp[index]=node; //2 node=next; } } //3.替換掉老array array=temp; defaultLength=temp.length; temp=null; } private int position(K k,int length) { int code=k.hashCode(); //取模算法 return code % (length-1); //求與算法 //return code & (defaultLength-1); } public V get(K k) { if(array!=null) { int index=position(k,defaultLength); Node node=array[index]; //遍歷鏈表 while(node!=null) { //如果key值相同返回value if(node.getKey()==k) { return node.getValue(); } else //如果key值不同則調到下一個元素 { node=node.next; } } } return null; } } public static void main(String[] args) { HashMap map=new HashMap (); map.put("001號", "001"); map.put("002號", "002"); map.put("003號", "003"); map.put("004號", "004"); map.put("005號", "005"); map.put("006號", "006"); map.put("007號", "007"); map.put("008號", "008"); map.put("009號", "009"); map.put("010號", "010"); map.put("011號", "011"); map.print(); System.out.println("========>"+map.get("009號")); } }
參考資料
coolblog
阿里架構師帶你分析HashMap源碼實現原理
感謝!
以下來自n天后的我:
補充一下看到一個非常好的:點擊鏈接,值得學習
想要了解更多精彩新姿勢?請訪問我的個人博客 本篇為原創內容,已在個人博客率先發表,隨后CSDN,segmentfault,juejin同步發出。如有雷同,那真是緣分~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76632.html
摘要:數據結構重要成員變量代表整個哈希表。科普,解決多線程并行情況下使用鎖造成性能損耗的一種機制,操作包含三個操作數內存位置預期原值和新值。 ConcurrenHashMap 。下面分享一下我對ConcurrentHashMap 的理解,主要用于個人備忘。如果有不對,請批評。 HashMap嚴重的勾起了我對HashMap家族的好奇心,下面分享一下我對ConcurrentHashMap 的理解...
摘要:的工作原理是近年來常見的面試題。讓我們再來看看這些問題設計哪些知識點的概念中解決碰撞的方法和的應用,以及它們在中的重要性不可變對象的好處多線程的條件競爭重新調整的大小總結的工作原理基于原理,我們通過和方法儲存和獲取對象。 HashMap 的工作原理是近年來常見的 Java 面試題。幾乎每個 Java 程序員都知道 HashMap,都知道哪里要用 HashMap,知道Hashtable和...
摘要:本文是作者自己對中線程的狀態線程間協作相關使用的理解與總結,不對之處,望指出,共勉。當中的的數目而不是已占用的位置數大于集合番一文通版集合番一文通版垃圾回收機制講得很透徹,深入淺出。 一小時搞明白自定義注解 Annotation(注解)就是 Java 提供了一種元程序中的元素關聯任何信息和著任何元數據(metadata)的途徑和方法。Annotion(注解) 是一個接口,程序可以通過...
摘要:所以利用哈希表這種數據結構實現具體類時,需要設計個好的函數,使沖突盡可能的減少其次是需要解決發生沖突后如何處理。源碼剖析首先從構造函數開始講,遵循集合框架的約束,提供了一個參數為空的構造函數與有一個參數且參數類型為的構造函數。 本文章首發于個人博客,鑒于sf博客樣式具有賞心悅目的美感,遂發表于此,供大家學習、批評。本文還在不斷更新中,最新版可移至個人博客。? 繼上一篇文章Java集合...
閱讀 2217·2021-11-22 13:54
閱讀 3380·2019-08-29 12:25
閱讀 3444·2019-08-28 18:29
閱讀 3589·2019-08-26 13:40
閱讀 3278·2019-08-26 13:32
閱讀 962·2019-08-26 11:44
閱讀 2235·2019-08-23 17:04
閱讀 2973·2019-08-23 17:02