1.HashMap介紹
HashMap為Map接口的一個實現類,實現了所有Map的操作。HashMap除了允許key和value保存null值和非線程安全外,其他實現幾乎和HashTable一致。
HashMap使用散列存儲的方式保存kay-value鍵值對,因此其不支持數據保存的順序。如果想要使用有序容器可以使用LinkedHashMap。
在性能上當HashMap中保存的key的哈希算法能夠均勻的分布在每個bucket中的是時候,HashMap在基本的get和set操作的的時間復雜度都是O(n)。
在遍歷HashMap的時候,其遍歷節點的個數為bucket的個數+HashMap中保存的節點個數。因此當遍歷操作比較頻繁的時候需要注意HashMap的初始化容量不應該太大。 這一點其實比較好理解:當保存的節點個數一致的時候,bucket越少,遍歷次數越少。
另外HashMap在resize的時候會有很大的性能消耗,因此當需要在保存HashMap中保存大量數據的時候,傳入適當的默認容量以避免resize可以很大的提高性能。 具體的resize操作請參考下面對此方法的分析
HashMap是非線程安全的類,當作為共享可變資源使用的時候會出現線程安全問題。需要使用線程安全容器:
Map m = new ConcurrentHashMap();或者
Map m = Collections.synchronizedMap(new HashMap());
具體的HashMap會出現的線程安全問題分析請參考9中的分析。
2.數據結構介紹
HashMap使用數組+鏈表+樹形結構的數據結構。其結構圖如下所示。
3.HashMap源碼分析(基於JDK1.8)
3.1關鍵屬性分析
transient Node<K,V>[] table; //Node類型的數組,記我們常說的bucket數組,其中每個元素為鏈表或者樹形結構
transient int size;//HashMap中保存的數據個數
int threshold;//HashMap需要resize操作的閾值
final float loadFactor;//負載因子,用於計算threshold。計算公式為:threshold = loadFactor * capacity
其中還有一些默認值得屬性,有默認容量2^4,默認負載因子0.75等.用於構造函數沒有指定數值情況下的默認值。
3.2構造函數分析
HashMap提供了三個不同的構造函數,主要區別為是否傳入初始化容量和負載因子。分別文以下三個。
//此構造函數創建一個空的HashMap,其中負載因子為默認值0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//傳入默認的容量大小,創造一個指定容量大小和默認負載因子為0.75的HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//創建一個指定容量和指定負載因為HashMap,以下代碼刪除了入參檢查
public HashMap(int initialCapacity, float loadFactor) {
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
注意:此處的initialCapacity為數組table的大小,即bucket的個數。
其中在指定初始化容量的時候,會根據傳入的參數來確定HashMap的容量大小。
初始化this.threshold的值為入參initialCapacity距離最近的一個2的n次方的值。取值方法如下:
case initialCapacity = 0:
this.threshold = 1;
case initialCapacity為非0且不為2的n次方:
this.threshold = 大於initialCapacity中第一個2的n次方的數。
case initialCapacity = 2^n:
this.threshold = initialCapacity
具體的計算方法為tableSizeFor(int cap)函數。計算方法是將入參的最高位下面的所有位都設置為1,然后加1
下面以入參為134217729為例分析計算過程。
首先將int轉換為二進制如下:
cap = 0000 1000 0000 0000 0000 0000 0000 0001
static final int tableSizeFor(int cap) {
int n = cap - 1; //n =0000 1000 0000 0000 0000 0000 0000 0000 此操作為了處理cap=2的n次方的情況,如果不減1,計算的結果位2的n+1方,添加次操作當cap = 2^n的時候計算結果為2^n
//以下操作將n的最高位到最低位之間的各位全部設置為1
// n = 0000 1000 0000 0000 0000 0000 0000 0000
|0000 01000 0000 0000 0000 0000 0000 000
= 0000 1100 0000 0000 0000 0000 0000 0000
//最高兩位設置為1
n |= n >>> 1;
// n = 0000 1100 0000 0000 0000 0000 0000 0000
| 0000 0011 0000 0000 0000 0000 0000 0000
= 0000 1111 0000 0000 0000 0000 0000 0000
//最高四位設置為1
n |= n >>> 2;
//n = 0000 1111 0000 0000 0000 0000 0000 0000
| 0000 0000 1111 0000 0000 0000 0000 0000
= 0000 1111 1111 0000 0000 0000 0000 0000
//最高8為設置為1
n |= n >>> 4;
//n = 0000 1111 1111 0000 0000 0000 0000 0000
| 0000 0000 0000 1111 1111 0000 0000 0000
= 0000 1111 1111 1111 1111 0000 0000 0000
//最高16為設置為1
n |= n >>> 8;
//n = 0000 1111 1111 1111 1111 0000 0000 0000
| 0000 0000 0000 0000 0000 1111 1111 1111
= 0000 1111 1111 1111 1111 1111 1111 1111
//不足32位,最高28位設置位1
n |= n >>> 16;
//n = n + 1 = 0000 1111 1111 1111 1111 1111 1111 1111 + 1 = 0001 0000 0000 0000 0000 0000 0000 0000
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
另外此處賦值為this.threshold,是因為構造函數的時候並不會創建table,只有實際插入數據的時候才會創建。目的應該是為了節省內存空間吧。
在第一次插入數據的時候,會將table的capacity設置為threshold,同時將threshold更新為loadFactor * capacity
3.3關鍵函數源碼分析
3.3.1 第一次插入數據的操作
HashMap在插入數據的時候傳入key-value鍵值對。使用hash尋址確定保存數據的bucket。當第一次插入數據的時候會進行HashMap中容器的初始化。具體操作如下:
Node<K,V>[] tab;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
其中resize函數的源碼如下,主要操作為根據cap和loadFactory創建初始化table
Node<K, V>[] oldTab = table;
int oldThr = threshold; //oldThr 根據傳入的初始化cap決定 2的n次方
int newCap, newThr = 0;
if (oldThr > 0) // 當構造函數中傳入了capacity的時候
newCap = oldThr; //newCap = threshold 2的n次方,即構造函數的時候的初始化容量
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
float ft = (float)newCap * loadFactor; // 2的n次方 * loadFactory
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
threshold = newThr; //新的threshold== newCap * loadFactory
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //長度為2的n次方的數組
table = newTab;
在初始化table之后,將數據插入到指定位置,其中bucket的確定方法為:
i = (n - 1) & hash // 此處n-1必定為 0000 1111 1111....的格式,取&操作之后的值一定在數組的容量范圍內。
其中hash的取值方式為:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
具體操作如下,創建Node並將node放到table的第i個元素中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = new Node(hash, key, value, null);
3.3.2 非第一次插入數據的操作源碼分析
當HashMap中已有數據的時候,再次插入數據,會多出來在鏈表或者樹中尋址的操作,和當size到達閾值時候的resize操作。多出來的步驟如下:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = new Node(hash, key, value, null);
else {
Node<K,V> e; K k;
// hash相等,且key地址相等或者equals為true的時候直接替換
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 當bucket的此節點為樹結構的時候,在樹中插入一個節點
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//便利此bucket節點,插入到鏈表尾部
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//當鏈表節點樹大於TREEIFY_THRESHOLD的時候,轉換為樹形結構
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();
另外,在resize操作中也和第一次插入數據的操作不同,當HashMap不為空的時候resize操作需要將之前的數據節點復制到新的table中。操作如下:
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;
}
}
}
}
}
3.4Cloneable和Serializable分析
在HashMap的定義中實現了Cloneable接口,Cloneable是一個標識接口,主要用來標識 Object.clone()的合法性,在沒有實現此接口的實例中調用 Object.clone()方法會拋出CloneNotSupportedException異常。可以看到HashMap中重寫了clone方法。
HashMap實現Serializable接口主要用於支持序列化。同樣的Serializable也是一個標識接口,本身沒有定義任何方法和屬性。另外HashMap自定義了
private void writeObject(java.io.ObjectOutputStream s) throws IOException
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
兩個方法實現了自定義序列化操作。
注意:支持序列化的類必須有無參構造函數。這點不難理解,反序列化的過程中需要通過反射創建對象。
4.HashMap的遍歷
以下討論兩種遍歷方式,測試代碼如下:
方法一:
通過map.keySet()獲取key的集合,然后通過遍歷key的集合來遍歷map
方法二:
通過map.entrySet()方法獲取map中節點集合,然后遍歷此集合遍歷map
測試代碼如下:
public static void main(String[] args) throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("name", "test");
map.put("age", "25");
map.put("address", "HZ");
Set<String> keySet = map.keySet();
for (String key : keySet) {
System.out.println(map.get(key));
}
Set<Map.Entry<String, Object>> set = map.entrySet();
for (Map.Entry<String, Object> entry : set) {
System.out.println("key is : " + entry.getKey() + ". value is " + entry.getValue());
}
}
輸出如下:
HZ
test
25
key is : address. value is HZ
key is : name. value is test
key is : age. value is 25
我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3c3z5rvu0g00g