hashmap为什么是2的倍数_HashMap源码解析(jdk1.8)
HashMap在java中使用的頻率很高,同時也是面試時的必問的問題。今天咱們就來學習下jHashMap的源碼,版本為jdk1.8。學習之前,先一起了解下HashMap的數據結構,便于理解后面所講的內容。
HashMap的底層數據結構
由圖可見,HashMap主要是由 數組+鏈表+紅黑樹 構成的。最外層是一個數組,數組中的每一個元素稱作桶(segment),每個桶中存在著鏈表或紅黑樹,其中鏈表或紅黑樹中的每一個元素又稱作bin。
簡單的描述下put的步驟。往map中put鍵值對時,首先計算鍵值對中key的hash值,以此確定插入數組中的位置(也就是下標值),但是可能存在同一hash值的元素已經被放在數組同一位置了,這種現象稱為碰撞,這時按照尾插法(jdk1.7及以前為頭插法)的方式添加key-value到同一hash值的元素的后面,鏈表就這樣形成了。當鏈表長度超過8時,鏈表自動轉換為紅黑樹。
靜態全區變量
/*** 默認初始化容量,值為16* 必須是2的n次冪.*/ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** 最大容量, 容量不能超出這個值。如果一個更大的初始化容量在構造函數中被指定,將被MAXIMUM_CAPACITY替換.* 必須是2的倍數。最大容量為1<<30,即2的30次方。*/ static final int MAXIMUM_CAPACITY = 1 << 30;/*** 默認的加載因子。*/ static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 將鏈表轉化為紅黑樹的臨界值。* 當添加一個元素被添加到有至少TREEIFY_THRESHOLD個節點的桶中,桶中鏈表將被轉化為樹形結構。* 臨界值最小為8*/ static final int TREEIFY_THRESHOLD = 8;/*** 恢復成鏈式結構的桶大小臨界值* 小于TREEIFY_THRESHOLD,臨界值最大為6*/ static final int UNTREEIFY_THRESHOLD = 6;/*** 桶可能被轉化為樹形結構的最小容量。當哈希表的大小超過這個閾值,才會把鏈式結構轉化成樹型結構,否則僅采取擴容來嘗試減少沖突。* 應該至少4*TREEIFY_THRESHOLD來避免擴容和樹形結構化之間的沖突。*/ static final int MIN_TREEIFY_CAPACITY = 64;一起走遍HashMap的流程(舉個栗子)
由于我們預計會放入一個元素,出于性能考慮,我們將容量設置為 2,既保證了性能,也節約了空間
/*** 初始化時進入的第一個方法*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}/*** 初始化時進入的第二個方法,傳入參數有(容量值,加載因子)* 流程解析:如果初始容量小于零,則拋出異常;如果初始容量大于最大容量,將最大容量值賦值給初始容量;如果加載因子小于零也會拋出異常* 接著對負載因子進行賦值,最后通過特定方法計算閥值(無論放入任何一個int 數字,都能找到離他最近的 2 的冪次方數字(并且比他大)并賦值*/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);}上面是 HashMap 的兩個構造方法,其中,我們設置了初始容量為 2, 而默認的加載因子我們之前說過:0.75,當然也可以自己設置,但 0.75 是最均衡的設置,沒有特殊要求不要修改該值,加載因子過小,理論上能減少 hash 沖突,加載因子過大可以節約空間,減少 HashMap 中最耗性能的操作:reHash。
2.往HashMap中put鍵值對
/*** put時進入的第一個方法*/ public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}/*** put時進入的第二個方法(計算key的hash值)* 流程解析:當key為null時,就返回零;不為null,則進入下一步計算,首先算出key的hashcode,當前key為“java”,則h=3254818,然后h* 異或h無符號右移16位的值,返回值為3254803*/ static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}/*** put時進入的第三個方法*/ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 當前對象的數組是null 或者數組長度時0時,則需要初始化數組if ((tab = table) == null || (n = tab.length) == 0)// 得到數組的長度 16n = (tab = resize()).length;// 如果通過hash值計算出的下標的地方沒有元素,則根據給定的key 和 value 創建一個元素if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { // 如果hash沖突了Node<K,V> e; K k;// 如果給定的hash和沖突下標中的 hash 值相等并且 (已有的key和給定的key相等(地址相同,或者equals相同)),說明該key和已有的key相同if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 那么就將已存在的值賦給上面定義的e變量e = p;// 如果以存在的值是個樹類型的,則將給定的鍵值對和該值關聯。else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 如果key不相同,只是hash沖突,并且不是樹,則是鏈表else { // 循環,直到鏈表中的某個節點為null,或者某個節點hash值和給定的hash值一致且key也相同,則停止循環。for (int binCount = 0; ; ++binCount) {// 如果next屬性是空if ((e = p.next) == null) {// 那么創建新的節點賦值給已有的next 屬性p.next = newNode(hash, key, value, null);// 如果樹的閥值大于等于7,也就是,鏈表長度達到了8(從0開始)。if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st// 如果鏈表長度達到了8,且數組長度小于64,那么就重新散列,如果大于64,則創建紅黑樹treeifyBin(tab, hash);// 結束循環break;}// 如果hash值和next的hash值相同且(key也相同)if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// 結束循環break;// 如果給定的hash值不同或者key不同。// 將next 值賦給 p,為下次循環做鋪墊p = e;}}// 通過上面的邏輯,如果e不是null,表示:該元素存在了(也就是他們呢key相等)if (e != null) { // existing mapping for key// 取出該元素的值V oldValue = e.value;// 如果 onlyIfAbsent 是 true,就不要改變已有的值,這里我們是false。// 如果是false,或者 value 是nullif (!onlyIfAbsent || oldValue == null)// 將新的值替換老的值e.value = value;// 訪問后回調afterNodeAccess(e);// 返回之前的舊值return oldValue;}}// 如果e== null,需要增加 modeCount 變量,為迭代器服務。++modCount;// 如果數組長度大于了閥值if (++size > threshold)// 重新散列resize();// 插入后回調afterNodeInsertion(evict);// 返回nullreturn null;}該方法為 HashMap 的核心方法,以下是該方法的步驟。
①判斷數組是否為空,如果是空,則創建默認長度位 16 的數組。
②通過與運算計算對應 hash 值的下標,如果對應下標的位置沒有元素,則直接創建一個。
③如果有元素,說明 hash 沖突了,則再次進行 3 種判斷。
1.判斷兩個沖突的key是否相等,equals 方法的價值在這里體現了。如果相等,則將已經存在的值賦給變量e。最后更新e的
value,也就是替換操作。
2.如果key不相等,則判斷是否是紅黑樹類型,如果是紅黑樹,則交給紅黑樹追加此元素。
3.如果key既不相等,也不是紅黑樹,則是鏈表,那么就遍歷鏈表中的每一個key和給定的key是否相等。如果,鏈表的長度
大于等于8了,則將鏈表改為紅黑樹,這是Java8 的一個新的優化。
④最后,如果這三個判斷返回的 e 不為null,則說明key重復,則更新key對應的value的值。
⑤對維護著迭代器的modCount 變量加一。
⑥最后判斷,如果當前數組的長度已經大于閾值了。則重新hash。
鏈表列下第二個菱形的條件中,加一個轉為為紅黑樹時還要判斷table.length 是否小于 MIN_TREEIFY_CAPACITY=64的條件3.根據鍵get值
/*** get時進入的第一個方法* 返回指定的key映射的value,如果value為null,則返回null。*/ public V get(Object key) {Node<K,V> e;//如果通過key獲取到的node為null,則返回null,否則返回node的value。getNode方法的實現就在下面。return (e = getNode(hash(key), key)) == null ? null : e.value; }get(E e)可以分為三個步驟:
getNode方法又可分為以下幾個步驟:
①如果哈希表為空,或key對應的桶為空,返回null
②如果桶中的第一個節點就和指定參數hash和key匹配上了,返回這個節點。
③如果桶中的第一個節點沒有匹配上,而且有后續節點
1.如果當前的桶采用紅黑樹,則調用紅黑樹的get方法去獲取節點
2.如果當前的桶不采用紅黑樹,即桶中節點結構為鏈式結構,遍歷鏈表,直到key匹配
④找到節點返回null,否則返回null。
3.resize() 擴容機制
聲明一個hashmap時不給它一個容量值時,hashmap會默認的容量值為16。若聲明時給定的容量值非2的n次冪,則會自動轉為2的n次冪,比如初始值給的5,hashmap會自動轉換為8。
如果 put值的數量大于閾值時,hashmap就會執行擴容,其中閾值為數組長度*加載因子。比如我們使用hashmap的默認容量16時,這時閾值=0.75*16=12,接著我們再put第十三個數據時,hashmap就開始擴容,擴容之后的長度為原長度的2倍,也是32。擴容就是把原來的小水桶廢棄,直接用更大的水桶替換。
PS:部分圖文來源網絡(侵刪)
總結
以上是生活随笔為你收集整理的hashmap为什么是2的倍数_HashMap源码解析(jdk1.8)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信用卡逾期七天怎么办 四种补救办法你可以
- 下一篇: 从客户端*****中检测到有潜在危险的