摘要:判斷該首節(jié)點(diǎn)是否與插入的鍵值對(duì)的和一致,若一致則替換該節(jié)點(diǎn)的值為,否則進(jìn)入下一步判斷首節(jié)點(diǎn)是否為樹節(jié)點(diǎn),若是則調(diào)用樹節(jié)點(diǎn)的方法遍歷紅黑樹,否則遍歷鏈表。中的方法會(huì)在鏈表超過(guò)樹化閾值的時(shí)候,將鏈表轉(zhuǎn)化為紅黑樹。
前言
由于Java 1.7和Java 1.8的HashMap的HashMap中的put()和get()方法在實(shí)現(xiàn)上差異很大,所以本文將于分別分析這兩個(gè)版本的put()和get()f方法
下面將會(huì)分析這部分的源碼,如果覺得源碼分析內(nèi)容太啰嗦,可以跳過(guò)源碼部分,直接看源碼下面的總結(jié)。
put()方法源碼分析HashMap的put()方法是我們最常用的方法,但是put()方法是怎么工作的呢?
Java 1.7 put()方法public V put(K key, V value) { if (key == null)// 處理key為null的情況 return putForNullKey(value); // 計(jì)算key的hash值 int hash = hash(key); // 計(jì)算命中table的索引 int i = indexFor(hash, table.length); // 遍歷命中的鏈表 for (Entrye = table[i]; e != null; e = e.next) { Object k; // 存在key和hash值相同則替換value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 記錄結(jié)構(gòu)性變化 modCount++; // 增加新鏈表 addEntry(hash, key, value, i); // 上一次節(jié)點(diǎn)不存在,返回null return null; }
put()方法實(shí)際上是
若key為null時(shí),直接調(diào)用putForNullKey()方法。否則進(jìn)入下一步
調(diào)用hash()方法獲取key的hash值,進(jìn)入下一步
調(diào)用indexFor()計(jì)算命中的散列表table的索引
遍歷鏈表,如果鏈表不存在或鏈表不存在key和hash值相同的節(jié)點(diǎn),則創(chuàng)建新的鏈表或尾部添加節(jié)點(diǎn),否則替換對(duì)應(yīng)節(jié)點(diǎn)的value
putForNullKey()private V putForNullKey(V value) { // 遍歷鏈表,但是命中的散列表的索引和key的hash值為0 // 后續(xù)邏輯與`put()`類似 for (Entrye = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
putForNullKey只是將命中散列表table的索引和key的hash值都設(shè)置為0,其他邏輯與put()方法后續(xù)的邏輯一致。
indexFor()方法/** * 計(jì)算命中散列表的索引 */ static int indexFor(int h, int length) { // 等價(jià)于length%h return h & (length-1); }hash()方法
/** * hash值計(jì)算方法 */ final int hash(Object k) { int h = 0; // 使用替代的hash方法 if (useAltHashing) { if (k instanceof String) { // 為字符串則使用特定的hash方法 return sun.misc.Hashing.stringHash32((String) k); } // 使用特定的hash種子計(jì)算hash值 h = hashSeed; } h ^= k.hashCode(); // 這部分代碼是為了減少哈希碰撞 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }addEntry()方法
void addEntry(int hash, K key, V value, int bucketIndex) { // 判斷散列表是否需要擴(kuò)容或者未初始化 if ((size >= threshold) && (null != table[bucketIndex])) { // 散列表擴(kuò)容為原來(lái)的2倍 resize(2 * table.length); // 計(jì)算key的hash值,key為null則返回0 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 創(chuàng)建新的鏈表 // 如果鏈表已存在,則是將新節(jié)點(diǎn)插入頭部(頭插法) createEntry(hash, key, value, bucketIndex); }createEntry()方法
/** * 頭插法插入新的節(jié)點(diǎn) * 不需要判斷鏈表是否存在 */ void createEntry(int hash, K key, V value, int bucketIndex) { EntryJava 1.8 put()方法e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
/** * HashMap的put()方法支持key/value為null */ public V put(K key, V value) { //實(shí)際上是先調(diào)用HashMap的hash()方法獲取到key的hash值 //然后調(diào)用HashMap的putVal()方法 return putVal(hash(key), key, value, false, true); }
put()方法實(shí)際上是
調(diào)用hash()方法獲取到key的hash值
調(diào)用putVal()方法存儲(chǔ)key-value
核心方法是putVal()方法,下面我會(huì)先分析一下hash()方法,因?yàn)檫@個(gè)方法涉及到hash值這個(gè)關(guān)鍵屬性的計(jì)算。
hash()方法static final int hash(Object key) { int h; // key為null時(shí),hash值為0 // key不為null時(shí),調(diào)用key對(duì)象的hashCode()方法并通過(guò)位運(yùn)算異或和無(wú)符號(hào)右移將高位分散到低位 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hash()方法指定了null的hash值為0。這樣就可以支持key為null。
(h = key.hashCode()) ^ (h >>> 16)這段代碼通過(guò)位運(yùn)算異或和無(wú)符號(hào)右移將高位分散到低位,這樣做可以減少哈希碰撞的概率(這塊不是很清楚原理,是從方法注釋上了解到的)
putVal()方法/** * Map.put()方法的實(shí)際實(shí)現(xiàn) * * @param hash key的hash值 * @param key 鍵值對(duì)中的key * @param value 鍵值對(duì)中的value * @param onlyIfAbsent 如果為true,則鍵值對(duì)中的值已經(jīng)存在則不修改這個(gè)值 * @param evict 如果為false,則是處于創(chuàng)建模式 * @return 上一次的value,如果上一次的value不存在,則為null */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab用于暫存散列表table。p為散列表中對(duì)應(yīng)索引的鏈表的頭節(jié)點(diǎn)的指針。n存儲(chǔ)tab的長(zhǎng)度。i則為命中的散列表的索引 Node[] tab; Node p; int n, i; //給tab和n賦值 //當(dāng)tab為null或者tab的長(zhǎng)度n為0時(shí),觸發(fā)resize()來(lái)初始化tab if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //使用(n - 1) & hash(等價(jià)于hash%n)計(jì)算命中的散列表索引,同時(shí)判斷散列表對(duì)應(yīng)索引的鏈表是否存在 if ((p = tab[i = (n - 1) & hash]) == null) //散列表對(duì)應(yīng)索引的鏈表不存在則創(chuàng)建一個(gè)新的鏈表 tab[i] = newNode(hash, key, value, null); else {//散列表對(duì)應(yīng)索引的鏈表已存在 Node e; K k; // 判斷頭節(jié)點(diǎn)的hash值和key是否與入?yún)⒌膆ash值和key一致。需要注意,null的hash值為0 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 對(duì)應(yīng)的鍵值對(duì)已經(jīng)存在,記錄下來(lái) e = p; else if (p instanceof TreeNode)//判斷對(duì)應(yīng)的鏈表是否轉(zhuǎn)化為紅黑樹 //若是,則直接調(diào)用紅黑樹的putTreeVal()方法 e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); else {//鏈表的頭節(jié)點(diǎn)與新的鍵值對(duì)不重復(fù),即沒(méi)有發(fā)生哈希碰撞 for (int binCount = 0; ; ++binCount) {//遍歷鏈表 if ((e = p.next) == null) {//遍歷到尾節(jié)點(diǎn) //尾插法添加一個(gè)新的節(jié)點(diǎn) p.next = newNode(hash, key, value, null); //鏈表長(zhǎng)度大于閾值 if (binCount >= TREEIFY_THRESHOLD - 1) // 從-1開始,所以為閾值-1 // 將鏈表轉(zhuǎn)化為紅黑樹 treeifyBin(tab, hash); // 中斷循環(huán) break; } // 判斷當(dāng)前遍歷的節(jié)點(diǎn)的hash值和key是否與入?yún)⒌膆ash值和key一致,即key是否已經(jīng)存在 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // key已經(jīng)存在,中斷循環(huán) break; // 記錄當(dāng)前遍歷的節(jié)點(diǎn) p = e; } } if (e != null) { // Map中存在重復(fù)的key V oldValue = e.value;//記錄下舊值 if (!onlyIfAbsent || oldValue == null)//判斷值存在是否可以進(jìn)行修改以及舊值是否為null e.value = value;//修改該節(jié)點(diǎn)的值 afterNodeAccess(e);// 鏈表節(jié)點(diǎn)的回調(diào)方法,此處為空方法 return oldValue;//返回舊值 } } // HashMap發(fā)生結(jié)構(gòu)變化,變化次數(shù)累加 ++modCount; // 鍵值對(duì)個(gè)數(shù)自增,同時(shí)判斷是否達(dá)到擴(kuò)容的閾值 if (++size > threshold) resize(); // 鏈表節(jié)點(diǎn)的回調(diào)方法,此處為空方法 afterNodeInsertion(evict); // 此處返回null是因?yàn)殒湵硇略隽斯?jié)點(diǎn),所以上一次的值必然為null return null; }
putVal()方法的關(guān)鍵點(diǎn):
若table沒(méi)有初始化則調(diào)用reszie()方法初始化。
計(jì)算命中的散列表索引位置,公式為(n - 1) & hash(等價(jià)于hash%n)。其中n為散列表長(zhǎng)度,hash為插入的鍵值對(duì)的key的哈希值。
判斷散列表對(duì)應(yīng)索引中的首節(jié)點(diǎn)是否為null,若為null,則創(chuàng)建鏈表,否則進(jìn)入下一步。
判斷該首節(jié)點(diǎn)是否與插入的鍵值對(duì)的key和hash一致,若一致則替換該節(jié)點(diǎn)的值為value,否則進(jìn)入下一步
判斷首節(jié)點(diǎn)是否為樹節(jié)點(diǎn),若是則調(diào)用樹節(jié)點(diǎn)的putTreeVal()方法遍歷紅黑樹,否則遍歷鏈表。
遍歷紅黑樹時(shí),若存在key和hash相同的節(jié)點(diǎn)就替換對(duì)應(yīng)節(jié)點(diǎn)的值value,若不存在則插入新的樹節(jié)點(diǎn)。
遍歷鏈表時(shí),若存在key和hash相同的節(jié)點(diǎn)就替換對(duì)應(yīng)節(jié)點(diǎn)的值為value。若找不到key和hash相同的節(jié)點(diǎn),則鏈表尾部插入節(jié)點(diǎn),同時(shí)進(jìn)入下一步。
若當(dāng)前鏈表長(zhǎng)度大于或等于樹化閾值TREEIFY_THRESHOLD(8)時(shí),則將鏈表轉(zhuǎn)化為紅黑樹。
get()方法源碼分析除了HashMap的put()方法外,get()方法也是一個(gè)我們常用的方法,下面開始分析其關(guān)鍵的源碼。
Java 1.7 get()方法public V get(Object key) { if (key == null)// key為null時(shí)特殊處理 return getForNullKey(); // 關(guān)鍵獲取key對(duì)應(yīng)value的代碼 Entryentry = getEntry(key); return null == entry ? null : entry.getValue(); }
get()方法的關(guān)鍵點(diǎn)如下:
若key為null,則調(diào)用getForNullKey()方法獲取value,否則進(jìn)入下一步
調(diào)用getEntry()方法獲取對(duì)應(yīng)的Entry對(duì)象
對(duì)應(yīng)的Entry對(duì)象為null時(shí)返回null,否則調(diào)用getValue()返回其value
getForNullKey()private V getForNullKey() { // 命中散列表索引為0,無(wú)需計(jì)算key的hash值 // 遍歷命中的鏈表 for (EntrygetEntry()e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
final EntryJava 1.8 get()方法getEntry(Object key) { // 計(jì)算key的hash值,key為null時(shí)返回0 int hash = (key == null) ? 0 : hash(key); // 遍歷命中的鏈表 for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } // 鏈表不存在或鏈表中不存在key和hash一致的節(jié)點(diǎn) return null; }
/** * 返回key對(duì)應(yīng)的value,如果不存在則返回null */ public V get(Object key) { Nodee; return (e = getNode(hash(key), key)) == null ? null : e.value; }
get()方法實(shí)際上是
調(diào)用hash()方法獲取到key的hash值
調(diào)用getNode()方法通過(guò)key和hash獲取對(duì)應(yīng)的value。不存在則返回null
核心方法是getNode()方法,下面我會(huì)先分析一下getNode()方法。
getNode()方法/** * Map.get()方法的實(shí)際實(shí)現(xiàn) * @param hash key的哈希值 * @param key 查詢用的key * @return 節(jié)點(diǎn)或者是節(jié)點(diǎn)不存在是返回null */ final NodegetNode(int hash, Object key) { //tab用于暫存散列表table。first為散列表中對(duì)應(yīng)索引的鏈表的頭節(jié)點(diǎn)的指針。n存儲(chǔ)tab的長(zhǎng)度。i則為命中的散列表的索引 Node [] tab; Node first, e; int n; K k; //初始化方法內(nèi)的變量,同時(shí)嘗試命中散列表 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é)點(diǎn) return first;//頭節(jié)點(diǎn)符合直接返回頭節(jié)點(diǎn) if ((e = first.next) != null) {//是否只有一個(gè)節(jié)點(diǎn) if (first instanceof TreeNode)//判斷頭節(jié)點(diǎn)是否為紅黑樹節(jié)點(diǎn) return ((TreeNode )first).getTreeNode(hash, key);//改為遍歷紅黑樹 do {//遍歷鏈表是否有符合的節(jié)點(diǎn) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //不存在對(duì)應(yīng)的key,返回null return null; }
getNode()方法的關(guān)鍵點(diǎn):
若散列表table不為null且長(zhǎng)度大于0且其索引為(n - 1) & hash(等價(jià)于hash%n)的節(jié)點(diǎn)不為null。其中n為散列表長(zhǎng)度,hash為插入的鍵值對(duì)的key的哈希值。則進(jìn)入下一步,否則直接返回null
判斷首節(jié)點(diǎn)的key和hash是否與入?yún)⒁恢?,若相同則返回首節(jié)點(diǎn),否則進(jìn)入下一步。
判斷節(jié)點(diǎn)個(gè)數(shù)只有1個(gè),若是則返回null,否則進(jìn)入下一步
判斷首節(jié)點(diǎn)是否為樹節(jié)點(diǎn),若是則遍歷紅黑樹,否則為鏈表,進(jìn)入下一步
遍歷鏈表,檢索key和hash與入?yún)⑾嗤墓?jié)點(diǎn),若找到則返回該節(jié)點(diǎn),否則返回null
總結(jié)put()和get()方法是HashMap的常用方法,通過(guò)學(xué)習(xí)其源碼了解到HashMap是如何使用拉鏈法解決哈希沖突。而下面將會(huì)通過(guò)兩幅圖展示put()和get()的執(zhí)行過(guò)程:
Java 1.7
put()方法圖解
get()方法圖解
put()方法圖解
get()方法圖解
既然分析了Java 1.7和Java 1.8中HashMap的put()和get()
方法,當(dāng)然少不了對(duì)二者的比較:
Java 1.7的HashMap中存在很多重復(fù)的代碼。例如putForNullKey()和put()方法中重復(fù)的鏈表遍歷,大量重復(fù)的hash值計(jì)算邏輯等等。而在Java 1.8中則對(duì)這部分的代碼進(jìn)行了重構(gòu)。例如將putForNullKey()和put()方法重復(fù)的代碼整合成putVal()方法,hash()方法處理key為null時(shí)的情況。
Java 1.8中的put()方法會(huì)在鏈表超過(guò)樹化閾值的時(shí)候,將鏈表轉(zhuǎn)化為紅黑樹。而Java 1.7中則只有鏈表
Java 1.7的鏈表節(jié)點(diǎn)插入為頭插法(不需要判斷鏈表是否存在),而Java 1.8的鏈表節(jié)點(diǎn)插入則為尾插法。
Java 1.8增加了對(duì)putIfAbsent()方法(存在才進(jìn)行更新)的支持,詳情可以看putVal()中關(guān)于onlyIfAbsent參數(shù)的處理邏輯。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/73262.html
摘要:習(xí)慣在微信看技術(shù)文章,想要獲取更多的資源的同學(xué),可以關(guān)注微信公眾號(hào)。為了大家方便,剛新建了一下群,大家也可以去交流交流。謝謝支持了希望能多介紹給其他有需要的朋友 前言 聲明,本文用得是jdk1.8 前面已經(jīng)講了Collection的總覽和剖析List集合以及散列表、Map集合、紅黑樹還有HashMap基礎(chǔ)了: Collection總覽 List集合就這么簡(jiǎn)單【源碼剖析】 Map集合、...
前言 聲明,本文用得是jdk1.8 前面已經(jīng)講了Collection的總覽和剖析List集合以及散列表、Map集合、紅黑樹的基礎(chǔ)了: Collection總覽 List集合就這么簡(jiǎn)單【源碼剖析】 Map集合、散列表、紅黑樹介紹 本篇主要講解HashMap,以及涉及到一些與hashtable的比較~ 看這篇文章之前最好是有點(diǎn)數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ): Java實(shí)現(xiàn)單向鏈表 棧和隊(duì)列就是這么簡(jiǎn)單 二叉樹就...
摘要:每個(gè)消息都會(huì)被一個(gè)線程消費(fèi),同時(shí)最大并發(fā)量為。然后提交一個(gè)任務(wù)到線程池中,這個(gè)任務(wù)的內(nèi)容是從等待隊(duì)列中取出一個(gè),如果等待隊(duì)列為空,則刪除這個(gè)等待隊(duì)列的。小結(jié)本文分析了的久經(jīng)生產(chǎn)考驗(yàn)的核心組件線程池。 本文首發(fā)于泊浮目的專欄:https://segmentfault.com/blog... 前言 在ZStack中,最基本的執(zhí)行單位不僅僅是一個(gè)函數(shù),也可以是一個(gè)任務(wù)(Task。其本質(zhì)實(shí)現(xiàn)...
摘要:下面我來(lái)簡(jiǎn)單總結(jié)一下的核心要點(diǎn)底層結(jié)構(gòu)是散列表數(shù)組鏈表紅黑樹,這一點(diǎn)和是一樣的。是將所有的方法進(jìn)行同步,效率低下。而作為一個(gè)高并發(fā)的容器,它是通過(guò)部分鎖定算法來(lái)進(jìn)行實(shí)現(xiàn)線程安全的。 前言 聲明,本文用的是jdk1.8 前面章節(jié)回顧: Collection總覽 List集合就這么簡(jiǎn)單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡(jiǎn)單【源碼剖析】 LinkedHas...
摘要:值得位數(shù)有的次方,如果直接拿散列值作為下標(biāo)訪問(wèn)主數(shù)組的話,只要算法比較均勻,一般是很難出現(xiàn)碰撞的。但是內(nèi)存裝不下這么大的數(shù)組,所以計(jì)算數(shù)組下標(biāo)就采取了一種折中的辦法,就是將得到的散列值與數(shù)組長(zhǎng)度做一個(gè)與操作。 hashMap簡(jiǎn)單介紹 hashMap是面試中的高頻考點(diǎn),或許日常工作中我們只需把hashMap給new出來(lái),調(diào)用put和get方法就完了。但是hashMap給我們提供了一個(gè)絕佳...
閱讀 2342·2021-11-22 14:56
閱讀 1484·2021-09-24 09:47
閱讀 915·2019-08-26 18:37
閱讀 2834·2019-08-26 12:10
閱讀 1531·2019-08-26 11:55
閱讀 3153·2019-08-23 18:07
閱讀 2308·2019-08-23 14:08
閱讀 614·2019-08-23 12:12