<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
HashMap 是通過 put(key,value) 儲存,get(key)來獲取。當傳入 key 時,HashMap 會根據 key 的 hashCode() 方法計算出 hash 值,根據 hash 值將 value 儲存在 bucket(桶)裡。當計算出的 hash 值相同時,稱之為 hash 衝突。HashMap 的做法是用連結串列和紅黑樹儲存相同 hash 值的 value
預設情況下,陣列大小為 16,那麼當 HashMap 中元素個數超過 16 * 0.75 = 12(這個值就是程式碼中的 threshold 值,也叫做臨界值)的時候,就把陣列的大小擴充套件為 2*16 = 32,即擴大一倍,然後重新計算每個元素在陣列中的位置
0.75 這個值稱為負載因子,那麼為什麼負載因子為 0.75 呢? 這是通過大量實驗統計得出來的,如果過小,比如 0.5,那麼當存放的元素超過一半時就進行擴容,會造成資源的浪費;如果過大,比如 1,那麼當元素滿的時候才進行擴容,會使 get,put 操作的碰撞機率增加
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列號 private static final long serialVersionUID = 362498820763181265L; // 預設的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 預設的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹;+對應的table的最小大小為64,即MIN_TREEIFY_CAPACITY ;這兩個條件都滿足,會連結串列會轉紅黑樹 static final int TREEIFY_THRESHOLD = 8; // 當桶(bucket)上的結點數小於這個值時樹轉連結串列 static final int UNTREEIFY_THRESHOLD = 6; // 桶中結構轉化為紅黑樹對應的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 儲存元素的陣列,總是2的冪次倍 transient Node<k,v>[] table; // 存放具體元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的個數,注意這個不等於陣列的長度。 transient int size; // 每次擴容和更改map結構的計數器 transient int modCount; // 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容 int threshold; // 填充因子 final float loadFactor; }
連結串列節點(單連結串列)
Node 是 HashMap 的一個內部類,實現了 Map.Entry 介面,本質上是一個單連結串列的資料結構。連結串列中的每個節點就是一個 Node 物件
static class Node<k,v> implements Map.Entry<k,v> { final int hash; final K key; V value; Node<k,v> next; // 下一個節點 Node(int hash, K key, V value, Node<k,v> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + = + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判斷兩個node是否相等,若key和value都相等,返回true。可以與自身比較為true public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<!--?,?--> e = (Map.Entry<!--?,?-->)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
紅黑樹節點
紅黑樹比連結串列多了四個變數,parent 父節點、left 左節點、right 右節點、prev上一個同級節點
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> { TreeNode<k,v> parent; // 父節點 TreeNode<k,v> left; // 左子樹 TreeNode<k,v> right;// 右子樹 TreeNode<k,v> prev; // 刪除時需要取消下一個連結 boolean red; // 顏色屬性 TreeNode(int hash, K key, V val, Node<k,v> next) { super(hash, key, val, next); } // 返回當前節點的根節點 final TreeNode<k,v> root() { for (TreeNode<k,v> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } } // ......省略 }
位桶
儲存(位桶)的陣列
transient Node<K,V>[] table;
HashMap 類中有一個非常重要的欄位,就是 Node[] table,即雜湊桶陣列,明顯它是一個 Node 的陣列
陣列判斷
紅黑樹判斷
連結串列判斷
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // tab表示 Node<K,V>型別的陣列,p表示某一個具體的單連結串列 Node<K,V> 節點 Node<K,V>[] tab; Node<K,V> p; int n, i; // 判斷 table[] 是否為空,如果是空的就建立一個 table[],並獲取他的長度n if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // tab[i = (n - 1) & hash] 表示陣列中的某一個具體位置的資料 // 如果單連結串列 Node<K,V> p == tab[i = (n - 1) & hash]) == null, // 就直接 put 進單連結串列中,說明此時並沒有發生 Hash 衝突 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 說明索引位置已經放入過資料了,已經在單連結串列處產生了Hash衝突 Node<K,V> e; K k; // 判斷 put 的資料和之前的資料是否重複 if (p.hash == hash && // 進行 key 的 hash 值和 key 的 equals() 和 == 比較,如果都相等,則初始化節點 Node<K,V> e ((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 { // 如果不是紅黑樹,就遍歷每個節點,判斷單連結串列長度是否大於等於 7, // 如果單連結串列長度大於等於 7,陣列的長度小於 64 時,會優先選擇擴容 // 如果單連結串列長度大於等於 7,陣列的長度大於 64 時,才會選擇單連結串列--->紅黑樹 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 採用尾插法,在單連結串列中插入資料 p.next = newNode(hash, key, value, null); // 如果 binCount >= 8 - 1 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } // 判斷索引每個元素的key是否可要插入的key相同,如果相同就直接覆蓋 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // 此時說明 key 的 hash 值和 key 的 equals() 和 == 比較結果都相等 // 說明陣列或者單連結串列中有完全相同的 key // 只需要將 value 覆蓋,並將 oldValue 返回即可 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // 說明沒有key相同,因此要插入一個key-value,並記錄內部結構變化次數 ++modCount; // 判斷是否擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
首先定位鍵值對所在桶的位置,之後再對連結串列或紅黑樹進行查詢
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 1.如果表不是空的,並且要查詢索引處有值,就判斷位於第一個的key是否是要查詢的key if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 1.1.檢查要查詢的是否是第一個節點 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 1.2.沿著第一個節點繼續查詢 if ((e = first.next) != null) { // 1.2.1.如果是紅黑樹,那麼呼叫紅黑樹的方法查詢 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 1.2.2.如果是連結串列,那麼採用連結串列的方式查詢 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
HashMap 的刪除操作並不複雜,僅需三個步驟即可完成
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { // ------------------1. 查詢到要刪除的節點------------------ // tab當前陣列,n當前陣列容量,p根據hash從陣列上找到的節點,index p節點在陣列上的位置 Node<K,V>[] tab; Node<K,V> p; int n, index; // 當陣列存在且陣列容量大於0,且p節點存在 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; // 當 p 的 hash 等於引數 hash,且 p 的 key 等於引數 key // p節點就是當前要刪除的節點,將node指向p if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; // 當p節點不是要刪除的節點時,說明p節點上有紅黑樹或者連結串列 else if ((e = p.next) != null) { // p如果是紅黑樹,通過getTreeNode()查詢節點 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); // p是連結串列,迴圈連結串列查詢節點 else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } // ------------------2. 刪除查詢到的節點------------------ // 如果查詢到的node存在且machValue為false或v等於value if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 如果node是TreeNode則使用removeTreeNode刪除節點 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 如果node等於p,說明移除連結串列頭,將node的後節點放到陣列上 else if (node == p) tab[index] = node.next; // 說明移除的不是連結串列頭,根據上方的迴圈可得,node是p的後節點,將p的後節點指向node的後節點 else p.next = node.next; // 增加修改次數,減少當前陣列長度 ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
當桶中連結串列長度超過 TREEIFY_THRESHOLD(預設為 8)時,就會呼叫 treeifyBin() 方法進行樹化操作。
但此時並不一定會樹化,因為在 treeifyBin()方法中還會判斷 HashMap 的陣列容量是否大於等於 64。
如果容量大於等於 64,那麼進行樹化,否則優先進行擴容
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; /* * 如果元素陣列為空 或者 陣列長度小於 樹結構化的最小限制 * MIN_TREEIFY_CAPACITY 預設值64,對於這個值可以理解為:如果元素陣列長度小於這個值,沒有必要去進行結構轉換 * 當一個陣列位置上集中了多個鍵值對,那是因為這些key的hash值和陣列長度取模之後結果相同。(並不是因為這些key的hash值相同) * 因為hash值相同的概率不高,所以可以通過擴容的方式,來使得最終這些key的hash值在和新的陣列長度取模之後,拆分到多個陣列位置上。 */ if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 擴容 // 如果元素陣列長度已經大於等於了 MIN_TREEIFY_CAPACITY,那麼就有必要進行結構轉換了 // 根據hash值和陣列長度進行取模運算後,得到連結串列的首節點 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; // 定義首、尾節點 do { TreeNode<K,V> p = replacementTreeNode(e, null); // 將該節點轉換為 樹節點 if (tl == null) // 如果尾節點為空,說明還沒有根節點 hd = p; // 首節點(根節點)指向 當前節點 else { // 尾節點不為空,以下兩行是一個雙向連結串列結構 p.prev = tl; // 當前樹節點的 前一個節點指向 尾節點 tl.next = p; // 尾節點的 後一個節點指向 當前節點 } tl = p; // 把當前節點設為尾節點 } while ((e = e.next) != null); // 繼續遍歷連結串列 // 到目前為止 也只是把Node物件轉換成了TreeNode物件,把單向連結串列轉換成了雙向連結串列 // 把轉換後的雙向連結串列,替換原來位置上的單向連結串列 if ((tab[index] = hd) != null) hd.treeify(tab);//此處單獨解析 } }
hash 衝突:在 put 多個元素時,通過 key 的 hashCode() 方法計算出的值是一樣的,是同一個儲存地址。當後面的元素要插入到這個地址時,發現已經被佔用了,這時候就產生了 hash 衝突。當發生 hash 衝突時,會進行如下操作
當上述條件判斷,只要有一個返回 false 時,也就是說需要put 的 key 與已存在的 key 是不相同的,則 HashMap 會使用單連結串列在已有資料的後面(單連結串列中)插入新資料,存取的陣列下標元素作為連結串列的頭部。這種解決 Hash 衝突的方法被稱為拉鍊法
HashMap 遍歷時,取得資料的順序是完全隨機的,這樣會導致按照順序讀取的時候和存入的順序是不一樣的
public class MapTest { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); map.put("2020-10-1", "李軍"); map.put("2020-10-3", "李華"); map.put("2020-11-1", "李剛"); map.put("2020-10-9", "李奎"); for (String key : map.keySet()) { System.out.println(key + ":" + map.get(key)); } } } 結果: 2020-10-3 : 李華 2020-11-1 : 李剛 2020-10-1 : 李軍 2020-10-9 : 李奎
以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45