HashMap 隨記

HashMap 構造器


HashMap 共有四個構造器:

public HashMap(int initialCapacity, float loadFactor) {// 對于傳入的初始容量(loadFactor) 及 負載因子(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(int initialCapacity, float loadFactor),其中 loadFactor 取默認值 DEFAULT_LOAD_FACTOR
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
// 容量及負載因子皆使用默認值
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
// 調用 putMapEntries 將值存入當前 hashmap
public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);
}

在上面構造器中存在一個方法【tableSizeFor】,這個方法的作用是:返回給定目標容量的二次方大小。

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;
}

換句話說,HashMap 的默認容量為16,而容量是以2的次方擴充的(即使是自定義傳入,也一定會經過轉換,如傳入30,則返回32),一是為了提高性能使用足夠大的數組,二是為了能使用位運算代替取模預算;

HashMap 的實現原理

數據存儲方式示例圖:
在這里插入圖片描述

在 JDK1.8 中,HashMap采用【位桶(數組table)+鏈表+紅黑樹】實現,當鏈表長度超過閾值(8)時,將鏈表轉換為紅黑樹(table長度大于等于64),這樣大大減少了查找時間;鏈表長度大于8時轉化為紅黑樹,小于6時轉化為鏈表;

HashMap put 方法:

/*** Associates the specified value with the specified key in this map.* If the map previously contained a mapping for the key, the old* value is replaced.** @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with <tt>key</tt>, or*         <tt>null</tt> if there was no mapping for <tt>key</tt>.*         (A <tt>null</tt> return can also indicate that the map*         previously associated <tt>null</tt> with <tt>key</tt>.)*/
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

其中 hash 方法:

// 計算 hash code
static final int hash(Object key) {int h;// key.hashCode 的調用方法:Object 中的原生方法 public native int hashCode();return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

重頭戲,putVal 方法:

/*** 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;
}

頭大,分解來看:

總體結構:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;/*** tab 用于保存 table 引用* 1、若 tab 為 null 或者 tab 的長度為 0,則調用 resize 方法進行初始化或者擴容*/if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;/*** 2、到了這一步,tab 一定存在* i = (n - 1) & hash 確定元素存放在 tab 中的下標,p = tab[i] */// 2.1、若 p 為 null,表示當前 tab 的 i 位置空,則可以直接直接構建 Node 插入if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { // 2.2、若 p 不為 null,表示 tab 的 i 位置已經有值,則繼續進行內部判斷Node<K,V> e; K k;// ... 后續單獨理解}/*** modCount 自增,記錄操作行為次數* 3、++size > threshold,即判斷下一次增加一個結點后size是否大于最大容量,如果是,則進行一次擴容*/++modCount;if (++size > threshold)resize();// 插入成功時會調用的方法(默認實現為空)afterNodeInsertion(evict);return null;
}

從 1 ~ 3 共三個步驟中,理解 putVal 方法大的執行方向。其中最復雜的是 2.2 中 else 中 的內容:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// ...else {Node<K,V> e; K k;// 已知:p = tab[i],是 tab[i] 中鏈表或者紅黑樹第一個結點// 如果 p.hash 和 p.key 與傳入參數中的 hash 和 key 相同,表示對應 key 已經存在,則直接使用原結點,只需要后面改變value即可if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果 p 是紅黑樹結點類型,則將其插入 tab[i] 位置中的紅黑樹中else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else { // p 是鏈表結點類型,則將其插入 tab[i] 位置中的鏈表中// 循環,尾插法for (int binCount = 0; ; ++binCount) {// 鏈表尾部if ((e = p.next) == null) {// 構建新結點,并修改 p 的 next 指向p.next = newNode(hash, key, value, null);/*** TREEIFY_THRESHOLD:將鏈表轉換為紅黑樹的閾值(默認為8),超過該閾值執行 treeifyBin 方法* 注意:執行 treeifyBin 方法并不代表一定會將鏈表轉換為紅黑樹,它會根據 table 的總長度來決定,即:* 只有當 table 的長度大于等于 64 后,才會執行轉換紅黑樹操作,否則只會對 table 進行擴容*/if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 這里會判斷整條鏈表上的結點的key、Hash值與插入的元素的key、Hash值是否相等(前面只判斷了鏈表中的第一個結點 p)// 如果相等,同前面一樣,表示已經存在key值相同的結點【e = p.next,其中 e 已經賦值了】,則直接退出循環if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) {V oldValue = e.value;/*** public V put(K key, V value) {*     return putVal(hash(key), key, value, false, true);* }* 這里 onlyIfAbsent 為 false,則 !onlyIfAbsent 為 true,進而執行 e.value = value 【新值替換舊值】*/if (!onlyIfAbsent || oldValue == null)e.value = value;/*** 在HashMap中:void afterNodeAccess(Node<K,V> p) { }* 實際上,afterNodeAccess 是提供給LinkedHashMap類【繼承HashMap】使用,LinkedHashMap 可以保證輸入輸出順序的一致性* 類似的還有 afterNodeInsertion、afterNodeRemoval 這兩個方法*/afterNodeAccess(e); // 這里默認實現為空return oldValue;}}// ...return null;
} 

putVal 方法中還涉及一些其他方法,如:

  1. resize:初始化或加倍表大小。如果 table 為空,則會根據字段閾值中保持的初始容量目標進行分配。
  2. treeifyBin:判斷是否需要將鏈表替換為紅黑樹
    a. replacementTreeNode:將鏈表結點 Node 轉換為樹結點 TreeNode
    b. treeify:形成從該節點鏈接的節點的樹
  3. Node:鏈表結點
  4. TreeNode:紅黑樹結點,關于紅黑樹的實現及對應操作,后續有機會再另講

putVal 方法流程圖(僅用于輔助理解源碼):
在這里插入圖片描述

其他

關于其他的方法,如 get、entrySet、ketSet 等,都可以在理解上述代碼后再去看源碼即可

關于紅黑樹:需要首先理解紅黑樹概念,再回頭來看這里的源碼更有效果(TODO:紅黑樹及HashMap紅黑樹源碼理解)

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}/*** Returns root of tree containing this node.*/final TreeNode<K,V> root() {for (TreeNode<K,V> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;}}// ...省略幾百行
}

補充1:HashSet

HashSet 的底層實現是 HashMap,來看 HashSet 的無參構造器:

/*** Constructs a new, empty set; the backing <tt>HashMap</tt> instance has* default initial capacity (16) and load factor (0.75).*/
public HashSet() {map = new HashMap<>();
}

add 方法:

public boolean add(E e) {return map.put(e, PRESENT)==null;
}

這里是把傳入的值,作為 map 的 key 傳入,而 value 統一為 PRESENT【private static final Object PRESENT = new Object();】

HashSet 之所以可以保證數據不會重復,其關鍵在于調用了 HashMap 的 put 方法:

// ...
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); // 通過hash計算【add】方法傳入的e的下標i,若在table[i]不存在則構建結點保存
else {// 如果結點存在,則判斷 key 與 hash 值是否都相同,具體流程此處不再贅述Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// ...
}// ...

也就是說,HashSet 利用 HashMap 中 key 值不能重復的特性來保證其存入的值不會重復。

HashSet 中其他方法基本都基于 HashMap 的方法,此處不再贅述。

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

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

相關文章

Android Audio基礎——音頻配置xml文件加載(七)

通過前面的文章&#xff0c;我們知道在 AudioPolicyManager 初始化的時候回調用 loadConfig() 方法去加載 Audio 相關的配置信息&#xff0c;這里我們就來詳細看一下。 一、配置文件加載 1、AudioPolicyManager 源碼位置&#xff1a;/frameworks/av/services/audiopolicy/ma…

將下拉彈層渲染節點固定在觸發器的父元素中

將下拉彈層渲染節點固定在觸發器的父元素中 注意: 如果發現下拉菜單跟隨頁面滾動&#xff0c;或者需要在其他彈層中觸發 Select&#xff0c; 請嘗試使用 getPopupContainer{triggerNode > triggerNode.parentElement} 將下拉彈層渲染節點固定在觸發器的父元素中。

【MySQL】探索 MySQL 的 GROUP_CONCAT 函數

緣分讓我們相遇亂世以外 命運卻要我們危難中相愛 也許未來遙遠在光年之外 我愿守候未知里為你等待 我沒想到為了你我能瘋狂到 山崩海嘯沒有你根本不想逃 我的大腦為了你已經瘋狂到 脈搏心跳沒有你根本不重要 &#x1f3b5; 鄧紫棋《光年之外》 什么是 GRO…

遺傳算法與應用分析

遺傳算法的概念 簡單來說&#xff0c;遺傳算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09;是一種模擬自然進化過程的優化算法。它通過模擬生物進化的遺傳機制&#xff0c;通過選擇、交叉和變異等操作&#xff0c;逐代優化搜索空間中的解。遺傳算法最初由約翰霍蘭…

【面試題-001】什么是面向對象?

文章目錄 什么是面向對象&#xff1f;與面向過程的區別&#xff1f;哪些語言是面向對象 哪些是面向過程&#xff1f; 什么是面向對象&#xff1f; 面向對象&#xff08;Object-oriented&#xff09;是一種程序設計范例&#xff0c;它通過將數據與對數據操作的函數&#xff08;…

V90 PN伺服驅動器附加報文750詳細使用介紹(算法分析)

1、V90PN伺服驅動器轉矩控制(750報文) V90 PN伺服驅動器轉矩控制(750報文)_v90pn轉矩控制-CSDN博客文章瀏覽閱讀3.4k次,點贊2次,收藏3次。主要介紹通過標準報文加附加報文 750 實現發送驅動報文的控制字、速度給定、轉矩限幅及附加轉矩給定的功能,首先就是V90在博途環境下…

算法學習筆記——對數器

對數器 對數器的實現&#xff1a; 你想要測的方法a&#xff08;最優解&#xff09;實現復雜度不好但是容易實現的方法b&#xff08;暴力解&#xff09;實現一個隨機樣本產生器&#xff08;長度也隨機、值也隨機&#xff09;把方法a和方法b跑相同的輸入樣本&#xff0c;看看得…

分享5款.NET開源免費的Redis客戶端組件庫

前言 今天大姚給大家分享5款.NET開源、免費的Redis客戶端組件庫&#xff0c;希望可以幫助到有需要的同學。 StackExchange.Redis StackExchange.Redis是一個基于.NET的高性能Redis客戶端&#xff0c;提供了完整的Redis數據庫功能支持&#xff0c;并且具有多節點支持、異步編…

總結2024/6/3

省流&#xff0c;藍橋杯國優&#xff0c;還是太菜了&#xff0c;聽說都是板子題但是還是寫不出來&#xff0c;靠暴力好歹沒有爆0&#xff0c;還是得多練&#xff0c;明年加油了

JWT 簽名用對稱加密還是非對稱加密?

一 概念梳理 對稱加密和非對稱加密是兩種基本的加密方法&#xff0c;它們在現代密碼學中扮演著核心角色&#xff0c;用于保護數據的安全和隱私。 1.1 對稱加密&#xff08;Symmetric Encryption&#xff09; 對稱加密是指加密和解密使用同一個密鑰的過程。這意味著發送方和接…

!力扣 108. 將有序數組轉換為二叉搜索樹

給你一個整數數組 nums &#xff0c;其中元素已經按升序排列&#xff0c;請你將其轉換為一棵 平衡二叉搜索樹。 示例 1&#xff1a; 輸入&#xff1a;nums [-10,-3,0,5,9] 輸出&#xff1a;[0,-3,9,-10,null,5] 解釋&#xff1a;[0,-10,5,null,-3,null,9] 也將被視為正確答案…

封裝了一個使用UICollectionViewLayout 實現的吸附居左banner圖

首先查看效果圖 實現的原理就是通過自定義UICollectionView layout&#xff0c;然后 設置減速速率是快速就可以達到吸附的效果 _collectionView.decelerationRate UIScrollViewDecelerationRateFast; 下面貼出所有代碼 這里是.h // // LBMiddleExpandLayout.h // Liubo…

文章解讀與仿真程序復現思路——電力系統自動化EI\CSCD\北大核心《具有源荷不平衡特性的配電網智能軟開關和儲能聯合規劃》

本專欄欄目提供文章與程序復現思路&#xff0c;具體已有的論文與論文源程序可翻閱本博主免費的專欄欄目《論文與完整程序》 論文與完整源程序_電網論文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 電網論文源程序-CSDN博客電網論文源…

CTF_RE學習

學了一個 map&#xff08;&#xff09;函數的使用 import base64rawData "e3nifIH9b_CndH" target list(map(ord, rawData)) # map 函數將 rawData 中的每個字符傳遞給 ord 函數。ord 函數返回給定字符的 Unicode 碼點 print(target) # 打印 map 對象的內存地址&…

汽車線束搭鐵與接地

一、搭鐵與接地的概念 首先在這里解釋一下“搭鐵”與“接地”的概念&#xff0c;不要混為一團&#xff01; 先說接地&#xff0c;大地是可導電的&#xff0c;其電位通常取為零。電力系統和電氣裝置的中性點、電氣設備的外露導電部分及裝置外導電部分通過導體與大地相連&#xf…

MySQL數據庫的約束

MySQL對于數據庫存儲的數據, 做出一些限制性要求, 就叫做數據庫的"約束". 在每一列的 列名, 類型 后面加上"約束". 一. not null (非空) 指定某列不能存儲null值. 二. unique (唯一) 保證這一列的每行必須有唯一值. 我們可以看到, 給 table 的 sn 列插…

【微服務】docker部署redis,一主二從三哨兵,讀寫分離

配置redis讀寫分離 3臺虛擬機 創建目錄用于掛載 mkdir -p /root/redis/{conf,data,logs} #master配置文件 bind 0.0.0.0 //任何ip都能訪問 port 6379 //redis端口號 logfile "/data/redis.log" //日志文件存放位置&#xff0c;啟動redis之前設置為空&#xff…

prometheus docker部署

1.安裝Docker sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF {"registry-mirrors":["https://hub-mirror.c.163.com"] } EOF export DOWNLOAD_URL"https://hub-mirror.163.com/docker-ce" curl -fsSL https://ge…

TypeScript 中的聲明合并

1. 聲明合并的概念 聲明合并是指當 TypeScript 遇到多個同名的聲明時&#xff0c;會將它們合并為一個單一的聲明。這使得開發者可以分散地定義同一個實體的不同部分&#xff0c;最終將它們合并為一個整體。在進行聲明合并時&#xff0c;TypeScript 會根據不同類型的聲明進行不…

【LIN】STM32新能源汽車LIN通信實現過程

【LIN】STM32新能源汽車LIN通信實現過程 文章目錄 前言一、軟件二、接線圖三、硬件原理圖四、上位機五、PICO示波器串行解碼1.軟件中的LIN波特率設置-192002.PIC設置3.PIC串行解碼 六.引用總結 前言 【電機控制】直流有刷電機、無刷電機匯總——持續更新 使用工具&#xff1a;…