JAVA8 hashmap原始碼閱讀筆記(紅黑樹連結串列)
一:hashmap的13 個成員變數
- static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
-> 陣列預設初始容量:16 - static final int MAXIMUM_CAPACITY = 1 << 30;
-> 陣列最大容量2 ^ 30 次方 - static final float DEFAULT_LOAD_FACTOR = 0.75f;
-> 預設負載因子的大小:0.75 - static final int MIN_TREEIFY_CAPACITY = 64;
-> 樹形最小容量:雜湊表的最小樹形化容量,超過此值允許表中桶轉化成紅黑樹 - static final int TREEIFY_THRESHOLD = 8;
-> 樹形閾值:當連結串列長度達到8時,將連結串列轉化為紅黑樹 - static final int UNTREEIFY_THRESHOLD = 6;
-> 樹形閾值:當連結串列長度小於6時,將紅黑樹轉化為連結串列 - transient int modCount; -> hashmap修改次數
- int threshold; -> 可儲存key-value 鍵值對的臨界值 需要擴充時;值 = 容量 * 載入因子
- transient int size; 已儲存key-value 鍵值對數量
- final float loadFactor; -> 負載因子
- transient Set< Map.Entry< K,V >> entrySet; -> 快取的鍵值對集合
- transient Node< K,V>[] table; -> 連結串列陣列(用於儲存hashmap的資料)
二:構造方法
/**
* 預設無參構造方法:
* 初始容量16
* 負載因子0.75
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
/**
* 引數為初始容量
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR); //呼叫HashMap(int initialCapacity, float loadFactor) 構造方法
}
/**
* 引數為初始容量 和 負載因子
*/
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);
}
/**
* 設定可儲存的 key-value 鍵值對最大值
* 返回值為傳入引數最接近的且大於等於的2的n次方
* 傳入引數 3 -> 4
* 傳入引數 5 -> 8
* 傳入引數 32 -> 32
* 傳入引數 33 -> 64
*/
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 >= 1 << 30) ? 1 << 30 : n + 1;
}
/*
* 構造一個新的 HashMap與指定的相同的對映 Map 。
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
// 具體實現將map 內元素遍歷插入hash表中
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;// ft代表 在預設負載因子下,該hashmap的容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);// 相當於執行 (int)Math.floor(ft);
if (t > threshold)
threshold = tableSizeFor(t);// 重新設定臨界值
}
else if (s > threshold)
resize();
// 一次將元素插入 後面詳細講解
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false,evict);
}
}
}
三:Node< K,V > 儲存結構
// 用於儲存key-value鍵值
// Map.Entry僅定義一些方法 因此不列出詳細程式碼
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;
}
public final boolean equals(Object o) {
// 此處進行的地址的比較
if (o == this)
return true;
// 此處呼叫equal方法,若該類重寫equal方法則呼叫重寫後的equal方法
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;
}
}
四:put方法
/**
* K - key(鍵) V - Value (值)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* hashcode 計算方式
* 若未重寫hashcode方法 則呼叫object的hashcode方法
* 涉及資料結構紅黑樹 再此不詳細講解
* 當容量達到64 且 單個位置連結串列長度達到8 連結串列轉化紅黑樹
* 若 單個位置連結串列長度減小到6 將紅黑樹轉化會連結串列
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 插入key-value 鍵值對具體實現
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判斷 若hashmap內沒有值 則重構hashmap
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 若指定位置hashcode 未被佔用 則直接將該鍵值對插入
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);
// 若此時連結串列內長度大於等於7 將連結串列轉化為紅黑樹 並將節點插入
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(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 key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 若容量不足 則擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* 調整底層用於儲存資料的hashmap底層陣列長度
* 優化操作效率 -> 每次擴容 擴大為原來的2倍
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 是否可以擴容
if (oldCap > 0) {
// 若已經最大 沒辦法 讓他衝突去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 將容量翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果舊容量為 0 ,並且舊閾值>0,說明之前建立了雜湊表但沒有新增元素,初始化容量等於閾值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的閾值為 0 ,就得用 新容量*載入因子 重計算一次
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 將底層連結串列陣列 資料複製已經調整過大小的新連結串列資料
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
五:get方法
/**
* 傳入key值返回value值
* 若不存在則返回null
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 具體實現get-value 方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 若表不為空且長度不為0 且指定位置hash表內有元素
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 若key值相同 返回該節點
if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 若該節點有下一個節點 且該節點屬於紅黑樹節點
// 按照紅黑樹高效查詢
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 直到下一個節點為空 迴圈 對比key值
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 若陣列不存在hash演算法的值 返回null
return null;
}
六:remove 方法
/**
* 傳入key 移除該key-value鍵值對
* 若不存在 key 返回null
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// 具體實現 remove - key 方法
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 若表不為空且長度不為0 且指定位置hash表內有元素
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;
// 若key值相同 保留該節點
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 若該節點屬於紅黑樹 按照紅黑樹查詢方式
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 迴圈連結串列直到連結串列為空 依次對比key 若相同 保留該節點
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 將該節點移除
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
// 若陣列不存在hash演算法的值 返回null
return null;
}
七:putAll方法
/**
* 引數為map 要求key-value
* 對應的泛型要求 K -key(本類或子類)V -value(本類或子類)
*/
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
// 具體插入實現
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 若表為空 載入一些成員變數
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
// 重新調整大小
else if (s > threshold)
resize();
// 依次遍歷元素 插入資料
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
原始碼閱讀到此結束:以下是我對hashmap自己的一些理解
- hashmap基於雜湊表的 map介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。與hashtable區別是hashmap允許使用null作為key值,且執行緒不安全,hashtable不允許null作為key值執行緒安全,可以使用Collections.synchronizedMap使hashmap執行緒安全,在JDK1.5後建議使用ConcurrentHashMap,ConcurrentHashMap採用鎖分段機制,hashtable是一個執行緒訪問時,其他執行緒無法訪問,但ConcurrentHashMap是一個執行緒訪問某個指定陣列的某一個下標,該下標所在段不允許其他執行緒訪問,其他執行緒仍可訪問ConcurrentHashMap的其他下標。
- hashmap底層資料結構是基於陣列和連結串列(紅黑樹 )來實現的,它之所以有相當快的查詢速度主要是因為它是通過計算雜湊碼來決定儲存的位置。通過key值計算hash碼值,將其傳遞進陣列,若hash碼值衝突則採用連結串列連線,若該hashmap容量大於64且該連結串列長度大於8則將該連結串列轉化為紅黑樹,若連結串列長度減少到6時候,則將紅黑樹轉化回連結串列
- hashmap每次擴容擴大為原來的兩倍,但擴容操作需要將資料全部重新計算放入新表,很消耗資源,如果對資料含量有一定預估,建議初始化hashmap時指定容量
閱讀完原始碼:感覺自己萌萌噠,看一看面試題吧!
1、“你知道HashMap的工作原理嗎?” “你知道HashMap的get()方法的工作原理嗎?”
HashMap是基於hashing的原理,我們使用put(key, value)儲存物件到HashMap中,使用get(key)從HashMap中獲取物件。當我們給put()方法傳遞鍵和值時,我們先對鍵呼叫hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry物件。
2、“當兩個物件的hashcode相同會發生什麼?”
因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發生。因為HashMap使用LinkedList儲存物件,這個Entry(包含有鍵值對的Map.Entry物件)會儲存在LinkedList中。(當向 HashMap 中新增 key-value 對,由其 key 的 hashCode() 返回值決定該 key-value 對(就是 Entry 物件)的儲存位置。當兩個 Entry 物件的 key 的 hashCode() 返回值相同時,將由 key 通過 eqauls() 比較值決定是採用覆蓋行為(返回 true),還是產生 Entry 鏈(返回 false)。),此時若你能講解JDK1.8紅黑樹引入,面試官或許會刮目相看。
3、“如果兩個鍵的hashcode相同,你如何獲取值物件?”
當我們呼叫get()方法,HashMap會使用鍵物件的hashcode找到bucket位置,然後獲取值物件。如果有兩個值物件儲存在同一個bucket,將會遍歷LinkedList直到找到值物件。找到bucket位置之後,會呼叫keys.equals()方法去找到LinkedList中正確的節點,最終找到要找的值物件。(當程式通過 key 取出對應 value 時,系統只要先計算出該 key 的 hashCode() 返回值,在根據該 hashCode 返回值找出該 key 在 table 陣列中的索引,然後取出該索引處的 Entry,最後返回該 key 對應的 value 即可。)
4、“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?”
當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會建立原來HashMap大小的兩倍的bucket陣列,來重新調整map的大小,並將原來的物件放入新的bucket陣列中。這個過程叫作rehashing,因為它呼叫hash方法找到新的bucket位置。
5、“你瞭解重新調整HashMap大小存在什麼問題嗎?”
當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個執行緒都發現HashMap需要重新調整大小了,它們會同時試著調整大小。在調整大小的過程中,儲存在LinkedList中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在LinkedList的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那麼就死迴圈了。這個時候,你可以質問面試官,為什麼這麼奇怪,要在多執行緒的環境下使用HashMap呢?