HashMap源碼分析(JDK1.8)
一、HashMap簡介
HashMap是一種基於數組+鏈表+紅黑樹的數據結構,其中紅黑樹部分在JDK1.8後引入,當鏈表長度大於8的時候轉換為紅黑樹。
HashMap繼承於AbstractMap(Map的骨架實現類),實現Map接口。
HashMap因為采用hashCode的值存儲,所以性能一般情況下為O(1)。
HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。
HashMap線程不安全,如在多線程環境下可以使用Collections工具類將其轉換為線程安全,也可使用JDK提供的CocurrentHashMap,不建議使用HashTable,理由是並發性能一般。
二、HashMap存儲結構分析
HashMap用哈希表存儲元素,其中采用鏈地址法解決沖突。
Node<K,V>是HashMap的內部實現類,實現Map.Entry<K,V>。
源碼為:
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; 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; } }
再來看看HashMap其他的幾個屬性:
transient Node<K,V>[] table; //數組 transient int size; //實際存在鍵值對數 transient int modCount; //修改次數 int threshold; //所能容納的key-value對極限 final float loadFactor; //負載因子 默認為0.75不建議修改
其中table默認的構造長度是16,並且要求length是2的n次方,這是一種非常規的設計,主要是為了在取模和擴容時做優化,同時為了減少沖突,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程。
當數據量增大後,即便負載因子和Hash算法設計的再合理,也避免不了鏈表長度增加,如此一來就會影響HashMap的性能,所以JDK1.8開始引入了紅黑樹,利用紅黑樹高性能的增刪查改解決這一問題,這裏不討論紅黑樹。
三、HashMap的功能實現
1)構造HashMap
HashMap有四個構造函數:
以下就源碼逐個分析構造函數
public HashMap(int initialCapacity, float loadFactor) { //構造時傳入指定大小以及負載因子 if (initialCapacity < 0) //容量大小合法性判斷 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//最大容量只能為MAXIMUM_CAPACTITY,2的30次方 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); //設置閾值,當存儲數據量達到閾值,將進行擴容,擴大為原來的2倍 }
public HashMap(int initialCapacity) { //指定容量大小,負載因子使用默認值0.75,調用上面的構造器 this(initialCapacity, DEFAULT_LOAD_FACTOR); }
public HashMap() { //無參構造器,設置負載因子為默認值 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
public HashMap(Map<? extends K, ? extends V> m) { //包含子map的構造函數 this.loadFactor = DEFAULT_LOAD_FACTOR; //設置默認負載因子 putMapEntries(m, false); // 將傳入的map元素逐個加入到HashMap中 }
2)確定數組索引位置
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
這是JDK1.8中確定數組索引的方法,h = key.hashCode() 為第一步 取hashCode值,h ^ (h >>> 16) 為第二步 高位參與運算,這裏的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算。
3)HashMap的put方法
首先是邏輯分析,put方法的處理邏輯:
①.判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;
②.根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;
③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裏的相同指的是hashCode以及equals;
④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
⑤.遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
看源代碼實現:
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;
//tab為空則新建 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//計算index,並處理null if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k;
//通過hashcode值以及equals方法判斷key是否存在,是的話直接覆蓋value 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);
//如果鏈表長度大於8轉換為紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }
//key已經存在直接覆蓋value 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; }
4)擴容機制
Java中數組擴容是無法自動擴容的,方法是用新數組代替舊數組。融入了紅黑樹後,resize方法復雜度大大增加,這裏簡單說下1.7的處理的過程:
傳入新的容量--引用擴容前的數組--判斷是否達到最大容量--初始化新數組--復制數據--設置table屬性引用新的數組--修改閾值
5)HashMap的get方法
HashMap存儲元素是通過計算HashCode值確定精確索引存放的,但是不同的key是有可能出現有相同的HashCode值的,這時候他們會被存放在同一個桶中(bucket),
然後通過equals方法來確保key的唯一性。
因此當取出數據時,就要根據hash算法找到在數組的存儲位置,再根據equals方法從該桶中找出對應的數據。
明白了邏輯以後看源代碼實現:
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; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { 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); do { //在鏈表中獲取 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
針對HashMap的學習主要理解它的存儲結構,工作原理,工作原理可以通過put與get方法去理解,期間可能產生擴容;另外要明白hashcode和equals兩個方法在HashMap中
的作用。此外還有一些常用的方法containsKey,clear,remove,可以查看源碼學習。
HashMap源碼分析(JDK1.8)