HashMap的源碼解析

HashMap基于哈希表的Map接口實現,是以key-value存儲形式存在,即主要用來存放鍵值對。HashMap的實現不是同步的,這意味著它不是線程安全的。它的keyvalue都可以為null。此外,HashMap中的映射不是有序的。

???????JDK1.8 之前?HashMap由數組+鏈表組成的,數組是?HashMap的主體,鏈表則是主要為了解決哈希沖突(兩個對象調用的hashCode方法計算的哈希碼值一致導致計算的數組索引值相同)而存在的(“拉鏈法”解決沖突)。

JDK1.8 以后在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(或者紅黑樹的邊界值,默認為 8)并且當前數組的長度大于64時,此時此索引位置上的所有數據改為使用紅黑樹存儲。當鏈表長度小于等于 6 時,轉為鏈表。

???????這樣做的目的是因為數組比較小,盡量避開紅黑樹結構,這種情況下變為紅黑樹結構,反而會降低效率,因為紅黑樹需要進行左旋,右旋,變色這些操作來保持平衡 。同時數組長度小于64時,搜索時間相對要快些。所以綜上所述為了提高性能和減少搜索時間。

/*** The default initial capacity - MUST be a power of two.*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默認容量大小(必須是二的n次冪)/*** The maximum capacity, used if a higher value is implicitly specified* by either of the constructors with arguments.* MUST be a power of two <= 1<<30.*/static final int MAXIMUM_CAPACITY = 1 << 30;	//最大容量(必須是二的n次冪)/*** The load factor used when none specified in constructor.*/static final float DEFAULT_LOAD_FACTOR = 0.75f;	//默認負載因子(默認的0.75)/*** The bin count threshold for using a tree rather than list for a* bin.  Bins are converted to trees when adding an element to a* bin with at least this many nodes. The value must be greater* than 2 and should be at least 8 to mesh with assumptions in* tree removal about conversion back to plain bins upon* shrinkage.*/static final int TREEIFY_THRESHOLD = 8;	//當鏈表的值超過8則會轉紅黑樹(1.8新增)/*** The bin count threshold for untreeifying a (split) bin during a* resize operation. Should be less than TREEIFY_THRESHOLD, and at* most 6 to mesh with shrinkage detection under removal.*/static final int UNTREEIFY_THRESHOLD = 6;  //當鏈表的值小于6則會從紅黑樹轉回鏈表/*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.*///當Map里面的容量(即表長度)超過這個值時,鏈表才能進行樹形化 ,否則元素太多時會擴容,而不是樹形化 //為了避免進行擴容、樹形化選擇的沖突,這個值不能小于 4 * TREEIFY_THRESHOLD(因此樹形化有兩個條件,表長度 > 64 and 鏈表長度 > 8)static final int MIN_TREEIFY_CAPACITY = 64;	/* ---------------- Fields -------------- *//*** The table, initialized on first use, and resized as* necessary. When allocated, length is always a power of two.* (We also tolerate length zero in some operations to allow* bootstrapping mechanics that are currently not needed.)*/transient Node<K,V>[] table;	// table用來初始化,類似容器來存放元素(必須是二的n次冪)/*** Holds cached entrySet(). Note that AbstractMap fields are used* for keySet() and values().*/transient Set<Map.Entry<K,V>> entrySet;	// 用來存放緩存/*** The number of key-value mappings contained in this map.*/transient int size;	// Map中存儲的元素數量/*** The number of times this HashMap has been structurally modified* Structural modifications are those that change the number of mappings in* the HashMap or otherwise modify its internal structure (e.g.,* rehash).  This field is used to make iterators on Collection-views of* the HashMap fail-fast.  (See ConcurrentModificationException).*/transient int modCount;	// 用來記錄HashMap的修改次數/*** The next size value at which to resize (capacity * load factor).** @serial*/// (The javadoc description is true upon serialization.// Additionally, if the table array has not been allocated, this// field holds the initial array capacity, or zero signifying// DEFAULT_INITIAL_CAPACITY.)int threshold;	// 閾值,用來調整大小下一個容量的值(計算方式為容量*負載因子)/*** The load factor for the hash table.** @serial*/final float loadFactor;	// 負載因子,創建HashMap也可調整,比如你希望用更多的空間換取時間,可以把負載因子調的更小一些,減少碰撞。

HashMap在樹形化時需要滿足以下兩個條件:

  1. 表長度(數組長度)必須大于或等于64

  2. 鏈表長度必須大于或等于8

這兩個條件的設定是基于性能優化和內存管理的考慮。

(1)表長度大于或等于64

表長度是指哈希表的數組長度。當表長度較小時(小于64),HashMap會優先選擇擴容而不是樹形化。原因如下:

  • 擴容的代價較小:當表長度較小時,擴容操作(即重新分配數組并重新哈希所有鍵值對)的代價相對較小。擴容可以更均勻地分布鍵值對,減少鏈表長度,從而避免樹形化的開銷。

  • 避免過早樹形化:如果表長度較小時就進行樹形化,可能會導致內存浪費,因為樹形化需要額外的內存來存儲紅黑樹的節點結構。

(2)鏈表長度大于或等于8

鏈表長度是指某個桶中鏈表的節點數量。當鏈表長度較小時(小于8),樹形化的收益較小,因為鏈表的查找時間復雜度O(n)在這種情況下已經足夠高效。只有當鏈表長度較長時(大于或等于8),樹形化才能顯著提升性能。

擴容機制

觸發條件:

  • 容量(capacity):HashMap 內部用來存儲數據的數組大小。初始時,默認的容量為?16

  • 負載因子(load factor):負載因子是一個表示 HashMap 充滿程度的參數。當 HashMap 中存儲的元素數量達到容量乘以負載因子時,HashMap 會進行擴容操作。默認的負載因子是?0.75

  • 當向 HashMap 中添加元素時,如果元素的數量(size)超過了負載因子乘以當前容量(即?size > loadFactor * capacity),HashMap 就會進行擴容操作。

  • 擴容操作會將 HashMap 中的數組容量翻倍,并且重新計算每個元素在新數組中的位置。這個過程涉及到重新計算哈希值,并將元素放入新的數組位置。

  • 擴容操作比較耗時,因為需要重新計算哈希值,并且可能涉及到大量的數據復制。因此,盡量在使用 HashMap 時給定一個合適的初始容量,以減少擴容次數,提高性能。

?

hash函數

計算方式

對于key的hashCode做hash操作,無符號右移(>>>)16位然后做異或(^)運算。

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
hash碰撞

???????只要兩個元素的key計算的哈希碼值相同就會發生哈希碰撞。jdk8前使用鏈表解決哈希碰撞。jdk8之后使用鏈表+紅黑樹解決哈希碰撞。

???????因為HashMap是由數組加鏈表/紅黑樹組成。平時存儲的元素都是在這個數組中。我們也稱之為“hash桶”,那么計算出兩個相同的hash值,我們就需要使用equals比較內容,如果兩個內容不同。我們就需要在同一個位置存儲。按照常規思路是不可能實現的。于是乎,在每個hash桶的位置存放鏈表或者紅黑樹。以此來存儲我們的數據。當然如果equals相同,那么就覆蓋之前的值即可。

那為什么數組的容量總是2的n次冪

??當向HashMap中添加一個元素的時候,需要根據key的hash值,去確定其在數組中的具體位置。 HashMap為了存取高效,要盡量較少碰撞,就是要盡量把數據分配均勻,每個鏈表長度大致相同,這個實現就在把數據存到哪個鏈表中的算法。

???????這個算法實際就是取模,hash%length,計算機中直接求余效率不如位移運算。所以源碼中做了優化,使用 hash&(length-1),而實際上hash%length等于hash&(length-1)的前提是length是2的n次冪。

???????為什么這樣能均勻分布減少碰撞呢?2的n次方實際就是1后面n個0,2的n次方-1 實際就是n個1;

按位與運算:相同的二進制數位上,都是1的時候,結果為1,否則為零。

如果創建HashMap對象時,輸入的數組長度是10,不是2的冪,HashMap通過位移運算和或運算得到的肯定是2的冪次數,并且是離那個數最近的數字。

?

增加方法

put方法是比較復雜的,實現步驟大致如下:

  1. 先通過hash值計算出key映射到哪個桶;

  2. 如果桶上沒有碰撞沖突,則直接插入;

  3. 如果出現碰撞沖突了,則需要處理沖突:

    a:如果該桶使用紅黑樹處理沖突,則調用紅黑樹的方法插入數據;

    b:否則采用傳統的鏈式方法插入。如果鏈的長度達到臨界值,則把鏈轉變為紅黑樹;

  4. 如果桶中存在重復的鍵,則為該鍵替換新值value;

  5. 如果size大于閾值threshold,則進行擴容;

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}/*** Implements Map.put and related methods.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;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 {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;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}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;}

???????HashMap只提供了put用于添加元素,putVal方法只是給put方法調用的一個方法,并沒有提供給用戶使用。 所以我們重點看putVal方法。

???????我們可以看到在putVal()方法中key在這里執行了一下hash()方法,來看一下Hash方法是如何實現的。

static final int hash(Object key) {int h;/*1)如果key等于null:可以看到當key等于null的時候也是有哈希值的,返回的是0.2)如果key不等于null:首先計算出key的hashCode賦值給h,然后與h無符號右移16位后的二進制進行按位異或得到最后的     hash值*/return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

???????從上面可以得知HashMap是支持Key為空的,而HashTable是直接用Key來獲取HashCode所以key為空會拋異常。

???????其實上面就已經解釋了為什么HashMap的長度為什么要是2的冪因為HashMap 使用的方法很巧妙,它通過 hash & (table.length -1)來得到該對象的保存位,前面說過 HashMap 底層數組的長度總是2的n次方,這是HashMap在速度上的優化。

???????當 length 總是2的n次方時,hash & (length-1)運算等價于對 length 取模,也就是hash%length,但是&比%具有更高的效率。比如 n % 32 = n & (32 -1)。

我們先研究下key的哈希值是如何計算出來的。key的哈希值是通過上述方法計算出來的。

???????這個哈希方法首先計算出key的hashCode賦值給h,然后與h無符號右移16位后的二進制進行按位異或得到最后的 hash值。計算過程如下所示:

 static final int hash(Object key) {int h;/*1)如果key等于null:可以看到當key等于null的時候也是有哈希值的,返回的是0.2)如果key不等于null:首先計算出key的hashCode賦值給h,然后與h無符號右移16位后的二進制進行按位異或得到最后的     hash值*/return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

在putVal函數中使用到了上述hash函數計算的哈希值:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {。。。。。。。。。。。。。。if ((p = tab[i = (n - 1) & hash]) == null)//這里的n表示數組長度16。。。。。。。。。。。。。。}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/80524.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/80524.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/80524.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

論文精讀:大規模MIMO波束選擇問題的量子計算解決方案

論文精讀&#xff1a;大規模MIMO波束選擇問題的量子計算解決方案 概要&#xff1a; 隨著大規模多輸入多輸出系統&#xff08;MIMO&#xff09;在5G及未來通信技術中的應用&#xff0c;波束選擇問題&#xff08;MBS&#xff09;成為提升系統性能的關鍵。傳統的波束選擇方法面臨計…

DPIN河內AI+DePIN峰會:共繪藍圖,加速構建去中心化AI基礎設施新生態

近日&#xff0c;一場聚焦前沿科技融合的盛會——AIDePIN峰會在越南河內成功舉辦。此次峰會由DPIN、QPIN及42DAO等Web3領域的創新項目聯合組織&#xff0c;匯聚了眾多Web3行業領袖、技術專家與社區成員。峰會于2025年4月19日舉行&#xff0c;其核心議題圍繞去中心化物理基礎設施…

品牌公關如何邀請媒體采訪?|微信文案模版

傳媒如春雨&#xff0c;潤物細無聲&#xff0c;大家好&#xff0c;我是51媒體胡老師。 &#x1f4f8;?不論是舉行活動、展會、發布會、推介會&#xff0c;還是新店開業&#x1f389; 都需要邀約媒體出席活動并采訪報道&#x1f3a4;&#x1f4f0; 我們需要在活動前提醒媒體參…

影樓精修-手部青筋祛除算法解析

注意&#xff1a;本文樣例圖片為了避免侵權&#xff0c;均使用AIGC生成&#xff1b; 手部青筋祛除科普 手部青筋祛除是影樓精修中一個非常精細的工作&#xff0c;需要較高的修圖技巧&#xff0c;目前市面上很少有自動化的青筋祛除功能的&#xff0c;而像素蛋糕目測是第一個做到…

智慧景區國標GB28181視頻平臺EasyGBS視頻融合應用全場景解決方案

一、方案背景? 隨著旅游業的蓬勃發展&#xff0c;景區的規模不斷擴大&#xff0c;游客數量持續增長&#xff0c;對景區的安全管理和游客服務質量提出了更高要求。打造一個高效、智能的視頻監控及管理系統成為景區運營的關鍵。EasyGBS作為一款基于國標GB28181協議的視頻云服務…

dedecms織夢arclist標簽noflag屬性過濾多個參數

織夢dedecms系統arclist標簽noflag屬性默認是只能過濾一個參數&#xff0c;比如過濾推薦是noflagc&#xff0c;過濾有圖片的文章是noflagc&#xff0c;在模板制作過程中&#xff0c;有時候我們為了seo和避免重復&#xff0c;需要過濾多個參數。今天小編就來跟大家講講織夢dedec…

如何用go語言搭MCP

1.什么是MCP? MCP是“模型上下文協議(Model Context Protocol)”的簡稱,用一句簡單通俗易懂的話描述: 是一種讓 AI 模型能夠無縫連接到外部工具和數據源的標準化方式。想象它就像 AI 的“萬能接口”,能讓 AI 像用 USB 線連接設備一樣,輕松調用其他程序或服務。2.官方M…

js 的call 和apply方法用處

主要用于ECMAScript與宿主環境&#xff08;文檔對象&#xff08;DOM&#xff09;、瀏覽器對象&#xff08;BOM&#xff09;&#xff09;的交互中&#xff1b; 例子&#xff1a;function changeStyle(attr, value){ this.style[attr] value; } …

移動通信行業術語

英文縮寫英文全稱中文名稱解釋/上下文舉例IMSIP Multimedia SubsystemIP多媒體子系統SIPSession Initiation Protocol會話初始化協議常見小寫sip同。ePDG/EPDGEvolved Packet Data Gateway演進分組數據網關 EPDG是LTE&#xff08;4G&#xff09;和后續蜂窩網絡架構&#xff08;…

c++11新特性隨筆

1.統一初始化特性 c98中不支持花括號進行初始化&#xff0c;編譯時會報錯&#xff0c;在11當中初始化可以通過{}括號進行統一初始化。 c98編譯報錯 c11: #include <iostream> #include <set> #include <string> #include <vector>int main() {std:…

Spark-Streaming簡介 核心編程

1. Spark-Streaming概述 定義&#xff1a;用于處理流式數據&#xff0c;支持多種數據輸入源&#xff0c;可運用Spark原語運算&#xff0c;結果能保存于多處。它以離散化流&#xff08;DStream&#xff09;為抽象表示&#xff0c;是RDD在實時數據處理場景的封裝。 特點&#x…

SpringbootWeb開發(注解和依賴配置)

Lombok 工具 Spring Web web開發相關依賴 MyBatis Framework MyBatis驅動 MySQL Driver MySql驅動包 Restful 風格 Slf4j 記錄日志對象 RequestMapping(value “/depts”, method RequestMethod.GET) //指定請求方式為GET method 指定請求方式 GetMapping 限定請求方式為Get…

雜項知識點

雜項 1 激活函數1.1 sigmoid1.2 tanh1.3 Relu1.4 leakRelu 1 激活函數 常用的激活函數包括sigmoid tanh Relu leakRelu 1.1 sigmoid import torch import numpy as np import matplotlib.pyplot as plt # sigmoid tanh Relu leakRelu ## 1 sigmoid ### 1.1 代碼復現sig…

計算機組成原理:指令系統

計算機組成原理:指令集系統 指令集體系結構(ISA)ISA定義ISA包含的內容舉個栗子指令的基本組成(操作碼+地址碼)指令分類:地址碼的個數定長操作碼變長操作碼變長操作碼的原則變長操作碼的設計指令尋址尋址方式的目的尋址方式分類有效地址直接在指令中給出有效地址間接給出有效地…

Rust實現高性能目錄掃描工具ll的技術解析

Rust實現高性能目錄掃描工具ll的技術解析 一、項目概述 本項目使用Rust構建了一個類ls命令行工具&#xff0c;具備以下核心特性&#xff1a; 多格式文件信息展示并行目錄掃描加速人類可讀文件大小運行時性能統計交互式進度提示 二、技術架構 1. 關鍵技術棧 clap&#xff…

【深度強化學習 DRL 快速實踐】策略梯度算法 (PG)

PG&#xff08;1984&#xff0c;Sutton&#xff09; 核心改進點 策略梯度算法 (PG): 直接對策略函數進行建模&#xff0c;可以適用于連續的動作空間 model-free, on-policy, PG, stochastic 策略 核心改進點說明策略梯度優化通過Actor網絡直接優化策略&#xff0c;適應連續動作…

G1垃圾回收器中YoungGC和MixedGC的區別

在 G1 垃圾回收器中&#xff0c;Mixed GC 和 Young GC 的區別主要體現在以下幾個方面&#xff1a; 作用范圍 Young GC&#xff1a;僅針對年輕代中的Region進行回收&#xff0c;包括 Eden 區和 Survivor 區的 Region。Mixed GC&#xff1a;會回收所有年輕代的 Region 以及部分…

從LLM到AI Agent的技術演進路徑:架構解析與實現邏輯

人工智能技術正經歷從基礎語言模型到智能執行體的關鍵躍遷。解析LLM→RAG→Agent的技術演進三層架構&#xff0c;拆解大模型與知識庫、工具鏈的融合機理&#xff0c;揭示感知-決策-執行閉環系統的構建邏輯。通過架構范式解析、代碼實現示例及多模態實踐案例&#xff0c;為開發者…

commix

Commix 基礎用法和高級用法 基礎用法 Commix 是一個自動化的命令行注入工具&#xff0c;用于檢測和利用 Web 應用程序中的命令注入漏洞。以下是基本使用方法&#xff1a; 基本掃描 python commix.py -u "http://example.com/vuln.php?id1"指定注入點 python commi…

Git刪除指定歷史版本

問題&#xff1a; 在Git提交版本&#xff0c;有時有些小版本相比較于后續的大版本&#xff0c;都會包含&#xff0c;且后續存在的意義不太大&#xff0c;一般認為是可以刪除的。或者&#xff0c;中間一些版本有問題但是也提交了&#xff0c;拉取這些版本根本沒用&#xff0c;這…