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