Map接口結構
map
接口是一個雙邊隊列,擁有key
,value
兩個屬性,其中key
在存儲的集合中不允許重複,value
可以重複。
HashMap
特點
- 存儲結構在
jdk1.7
當中是數組加鏈表的結構,在jdk1.8
當中改爲了數組加鏈表加紅黑樹的結構。 HashMap
在多線程的環境下是不安全的,沒有進行加鎖措施,所以執行效率快。如果我麼需要有一個線程安全的HashMap
,可以使用Collections.synchronizedMap(Map<K,V> m)
方法獲得線程安全的HashMap
,也可以使用ConcurrentHashMap
類創建線程安全的map
。- 存儲的元素在
jdk1.7
當中是Entry
作爲存儲的節點,在jdk1.8
當中用Node
作爲存儲的節點,Node
實現了Map.Entry
接口。在map
中其實是將key
和value
節點組合起來成爲一個Node
節點作爲存儲。
// jdk1.8Node節點
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;
}
// 下面省略代碼
}
- 在
HashMap
中key
不允許重複,value
可以允許重複,並且key
和value
都允許是null
值,因爲他們是另外存儲的。 - 當我們使用自定義類作爲
HashMap
的key時我們需要重寫object
類的,hashCode()
和equals()
方法。 - 在
HashMap
中鏈表部分在jdk1.7
中新的節點總是在鏈表的頭部,舊的節點在新節點的next
域當中,而在jdk1.8
中是新的節點總是在鏈表的尾部,他們的指向都是由舊節點指向新的節點,由此我們可以記成七上八下
。
HashMap
當中常見的名詞
DEFAULT_INITIAL_CAPACITY
默認數組容量大小,16MAXIMUM_CAPACITY
最大數組容量大小,2^30DEFAULT_LOAD_FACTOR
默認的負載因子,0.75TREEIFY_THRESHOLD
鏈表改成紅黑樹存儲的最小長度MIN_TREEIFY_CAPACITY
鏈表改成紅黑樹存儲,map數組最小容量
HashMap
存儲元素過程源碼解析(jdk1.8)
// 存儲元素的數組,加上transient關鍵字代表不可以被序列化
transient Node<K,V>[] table;
// 這個方法是HashMap對外公開的添加元素方法
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) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 在jdk1.7當中,創建一個HashMap實例是直接分配一個長度爲16的數組,
// 在jdk1.8當中,創建HashMap時是分配了一個長度爲0的數組,然後調用put方法時,如果數組長
// 度爲0時,則調用數組擴容方法,擴容數組到長度爲16。當長度不爲0時,擴容操作是擴大到原來
// 長度的兩倍
if ((tab = table) == null || (n = tab.length) == 0)
// 調用擴容方法,並利用n變量,記錄數組的長度
n = (tab = resize()).length;
// 通過計算哈希散列,得出該key應該放在數組的哪一個位置上,用變量p存儲該位置上的元素
// 判斷該位置上是否已經存在元素,如果不存在元素則直接將該元素放入數組中,存放
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果數組的該位置已經存在了元素
else {
Node<K,V> e; K k;
// 比較需要加入的元素,於原來的元素hash值進行比較,如果hash值和equals都爲true那麼
// 判定兩個元素爲相同元素,用變量e來記錄原來的元素,方便下面進行替換
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果上述條件不滿足,但是是一個TreeNode類型,則當作樹節點插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 當hash值不同,或者equals爲false,也不是TreeNode時將新元素插入數組對應位置的
// 鏈表中,遍歷對應數組位置的鏈表
for (int binCount = 0; ; ++binCount) {
// 用變量e存儲鏈表的下一個節點
if ((e = p.next) == null) {
// 如果該鏈表只有一個元素,則直接將舊節點的next域指向新的節點
p.next = newNode(hash, key, value, null);
// 當單條鏈表上元素數量大於最大數量時則按照紅黑樹存儲8,在進行紅黑樹時
// 又會進行判斷數組容量是否到達紅黑樹最小容量64,兩個條件同時滿足,則該
// 條鏈表改造成紅黑樹存儲
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 比較鏈表的下一個節點的hash值和equals
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e不等於null則證明,在鏈表中存在相同的元素,則進行替換
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;
}
// 將鏈表改造成紅黑樹
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 判斷是否滿足條件,數組大小達到64,沒有達到則做擴容操作,達到則改成紅黑樹
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
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);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
HashMap
常見的問題
爲什麼在使用自定義類型作爲key
需要我們重寫hashCode()
和equals()方法
因爲map的存值的時候是先計算hash值,然後再判斷equals,通過這兩個值是否都爲true來判斷該元素是否再map中已經存在。如果不重寫這兩個方法,可能會存在我們認爲相同,但是他們的hash或者equals不同的元素也能存進map中,從而達不到key唯一的效果。
在HashMap
中爲什麼要存在DEFAULT_LOAD_FACTOR
負載因子這個概念
因爲我們map的存儲數據結構是數組加鏈表加紅黑樹結構,如果沒有負載因子,map是不可能滿的,所以加上一個負載因子的概念來判斷數組是否擴容,減少鏈表的負擔。