#HashMap
#####前言: 從今天開始我將介紹Map系列接口,我認為Map是集合類中概念最多,實現最復雜的一類接口。在講解過程中會涉及到不少數據結構的知識,這部分知識點需要讀者額外花一定時間系統學習。
HashMap是Map的一個實現類,這個類很重要,是很多集合類的實現基礎,底層用的就是他,比如前文中講到的HashSet,下文要講到的LinkedHashMap。我們可以將HashMap看成是一個小型的數字字典,他以key-value的方式保存數據,Key全局唯一,并且key和value都允許為null。
HashMap底層是通過維護一個數據來保存元素。當創建HashMap實例的時候,會通過指定的數組大小以及負載因子等參數創建一個空的數組,當在容器中添加元素的時候,首先會通過hash算法求得key的hash值,再根據hash值確定元素在數組中對應的位置,最后將元素放入數組對應的位置。在添加元素的過程中會出現hash沖突問題,沖突處理的方法就是判斷key值是否相同,如果相同則表明是同一個元素,替換value值。如果key值不同,則把當前元素添加到鏈表尾部。這里引出了一個概念,就是HashMap的數據結構其實是:hash表+單向鏈表。通過鏈表的方式把所有沖突元素放在了數組的同一個位置。但是當鏈表過長的時候會影響HashMap的存取效率。因此我們在實際使用HashMap的時候就需要考慮到這個問題,那么該如何控制hash沖突的出現頻率呢?HashMap中有一個負載因子(loadFactor)的概念。容器中實際存儲元素的size = loadFactor * 數組長度,一旦容器元素超出了這個size,HashMap就會自動擴容,并對所有元素重新執行hash操作,調整位置。好了說了這么多,下面就開始介紹源碼實現。
#####一、Node結構介紹 Node類實現了Map.Entry接口,他是用于存放數據的實體,是容器中存放數據的最小單元。Node的數據結構是一個單向鏈表,為什么選用這種結構?那是因前文講到的,HashMap存放數據的結構是:hash表+單向鏈表。下面給出定義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;}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;} }這個結構非常簡單,定義了一個hash和key,hash值是對key進行hash以后得到的。value保存實際要存儲的對象。next指向下一個節點。當hash沖突以后,就會將沖突的元素放入這個單向鏈表中。
#####二、創建HashMap 創建HashMap實例有四個構造方法,這里著重介紹一個,看源碼:
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); }// HashMap的數組大小是有講究的,他必須是2的冪,這里通過一個牛逼哄哄的位運算算法,找到大于或等于initialCapacity的最小的2的冪 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 >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }構造方法中有兩個參數,第一個initialCapacity定義map的數組大小,第二個loadFactor意為負載因子,他的作用就是當容器中存儲的數據達到loadFactor限度以后,就開始擴容。如果不設定這樣參數的話,loadFactor就等于默認值0.75。但是細心的你會發現,容器創建以后,并沒有創建數組,原來table是在第一次被使用的時候才創建的,而這個時候threshold = initialCapacity * loadFactor。 這才是這個容器的真正的負載能力。
tableSizeFor這個方法的目的是找到大于或等于initialCapacity的最小的2的冪,這個算法寫的非常妙,值得我們細細品味。
假設cap=7
第一步 n = cap -1 = 6 = 00000110
第二步 n|= n>>>1:
n>>>1表示無符號右移1位,那么二進制表示為00000011,此時00000110 | 00000011 = 00000111
第三步 n|=n>>>2:
00000111 & 00000001 = 00000111
第四部 n|=n>>>4:
00000111 & 00000000 = 00000111
第五步 n|=n>>>8;
00000111 & 00000000 = 00000111
第六步 n|=n>>>16;
00000111 & 00000000 = 00000111
最后 n + 1 = 00001000
其實他的原理很簡單,第一步先對cap-1是因為如果cap原本就是一個2的冪,那么最后一步加1,會使得這個值變成原來的兩倍,但事實上原來這個cap就是2的冪,就是我們想要的值。接下來后面的幾步無符號右移操作是把高位的1補到低位,經過一系列的位運算以后的值必定是000011111...他的低位必定全是1,那么最后一步加1以后,這個值就會成為一個00010000...(2的冪次),這就是通過cap找到2的冪的方法。看到如此簡約高效的算法,我服了。
#####三、put添加元素 添加一個元素是所有容器中的標配功能,但是至于添加方式那就各有千秋,Map添加元素的方式是通過put,向容器中存入一個Key-Value對。下面我將詳細介紹put的實現過程,這個方法非常重要,吃透了這個方法的實現原理,基本也就能搞懂HashMap是怎么一回事了。
public V put(K key, V value) {return putVal(hash(key), key, value, false, true); }// 獲取key的hash值,這里講hash值的高16位右移和低16位做異或操作,目的是為了減少hash沖突,使hash值能均勻分布。 static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 如果是第一次添加元素,那么table是空的,首先創建一個指定大小的table。if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 通過對hash與數組長度的與操作,確定key對應的數組位置,然后讀取該位置中的元素。if ((p = tab[i = (n - 1) & hash]) == null)// 如果當前位置為空,那么就在當前數組位置,為這個key-value創建一個節點。tab[i] = newNode(hash, key, value, null);else {// 如果當前位置已經存在元素,那么就要逐個讀取這條鏈表的元素。Node<K,V> e; K k;// 如果key和hash值都等于當前頭元素,那么這存放的兩個元素是相同的。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 {for (int binCount = 0; ; ++binCount) {// 遍歷鏈表的所有元素,如果都未找到相同key的元素,那么說明這個元素并不在容器中存在,因此將他添加到鏈表尾部,并結束遍歷。if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 如果在遍歷過程中,發現了相同的key值,那么就結束遍歷。if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 如果e != null 說明在當前容器中,存在一個相同的key值,那么就要替換key所對應的valueif (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;// 這是專門留給LinkedHashMap調用的回調函數,LinkedHashMap會實現這個方法。從這里可以看出,HashMap充分的考慮了他的擴展性。afterNodeAccess(e);return oldValue;}}++modCount;// 這里判斷當前元素的數量是否超過了容量的上限,如果超過了,就要重新進行擴容,并對當前元素重新hash,所以再次擴容以后的元素位置都是會改變的。if (++size > threshold)resize();// 此方法也是HashMap留給LinkedHashMap實現的回調方法。透露一下,因為LinkedHashMap在插入元素以后,都會維護他的一個雙向鏈表afterNodeInsertion(evict);return null; }#####四、get獲取元素 使用HashMap有一個明顯的優點,就是他的存取時間開銷基本維持在O(1),除非在數據量大了以后hash沖突的元素多了以后,對其性能有一定的影響。那么現在介紹的get方法很好的體現了這個優勢。
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value; }// 同一個key的hash值是相同的,通過hash就可以求出數組的下標,便可以在O(1)的時間內獲取元素。 final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 在容器不為空,并且對應位置也存在元素的情況下,那么就要遍歷鏈表,找到相同key值的元素。if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 如果第一個元素的key值相同,那么這個元素就是我們要找的。if (first.hash == hash &&((k = first.key) == key || (key != null && key.equals(k))))return first;// 如果第一個元素不是我們要找的,接下來就遍歷鏈表元素,如果遍歷完了以后都沒找到,說明不存在這個key值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; }#####五、remove刪除元素 刪除元素的實現原理和put,get都類似。remove通過給定的key值,找到在hash表中對應的位置,然后找出相同key值的元素,對其刪除。
public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value; }// 通過key的hash值定位元素位置,并對其刪除。這里的實現和put基本相同,我只在不同的地方做一下解釋。 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;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;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 {do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}// 如果找到了相同的key,接下來就要判斷matchValue參數,matchValue如果是true的話,就說明// 需要檢查被刪除的value是否相同,只有相同的情況下才能刪除元素。如果matchValue是false的話// 就不需要判斷value是否相同。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;elsep.next = node.next;++modCount;--size;afterNodeRemoval(node);return node;}}return null; }#####六、resize動態擴容 resize這個方法非常重要,他在添加元素的時候就會被調用到。resize的目的是在容器的容量達到上限的時候,對其擴容,使得元素可以繼續被添加進來。這里需要關注兩個參數threshold和loadFactor,threshold表示容量的上限,當容器中元素數量大于threshold的時候,就要擴容,并且每次擴容都是原來的兩倍。loadFactor表示hash表的數組大小。這兩個參數的配合使用可以有效的控制hash沖突數量。
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 如果容器并不是第一次擴容的話,那么oldCap必定會大于0if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// threshold和數組大小cap共同擴大為原來的兩倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1;}// 第一次擴容,并且設定了threshold值。else if (oldThr > 0)newCap = oldThr;else {// 如果在創建的時候并沒有設置threshold值,那就用默認值newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {// 第一次擴容的時候threshold = cap * loadFactorfloat 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;// 如果不是第一次擴容,那么hash表中必然存在數據,需要將這些數據重新hashif (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// 這里分兩串,lo表示原先位置的所有,hi表示新的索引Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 因為cap都是2的冪次,假設oldCap == 10000,// 假設e.hash= 01010 那么 e.hash & oldCap == 0。// 老位置= e.hash & oldCap-1 = 01010 & 01111 = 01010// newCap此時為100000,newCap-1=011111。// 此時e.hash & newCap 任然等于01010,位置不變。// 如果e.hash 假設為11010,那么 e.hash & oldCap != 0// 原來的位置為 e.hash & oldCap-1 = 01010// 新位置 e.hash & newCap-1 = 11010 & 011111 = 11010// 此時 新位置 != 老位置 新位置=老位置+oldCap// 因此這里分類兩個索引的鏈表if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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; }#####七、遍歷 HashMap遍歷有三種方式,一種是對key遍歷,還有一種是對entry遍歷和對value遍歷。這三種遍歷方式都是基于對HashIterator的封裝,三種實現方式大同小異,因此我著重介紹EntryIterator的實現。
// 對HashMap元素進行遍歷。 public Set<Map.Entry<K,V>> entrySet() {Set<Map.Entry<K,V>> es;// 第一次遍歷的時候,實例化entrySet。return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }final class EntrySet extends AbstractSet<Map.Entry<K,V>> {public final int size() { return size; }public final void clear() { HashMap.this.clear(); }public final Iterator<Map.Entry<K,V>> iterator() {return new EntryIterator();}public final boolean contains(Object o) {if (!(o instanceof Map.Entry))return false;Map.Entry<?,?> e = (Map.Entry<?,?>) o;Object key = e.getKey();Node<K,V> candidate = getNode(hash(key), key);return candidate != null && candidate.equals(e);}public final boolean remove(Object o) {if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>) o;Object key = e.getKey();Object value = e.getValue();return removeNode(hash(key), key, value, true, true) != null;}return false;}public final Spliterator<Map.Entry<K,V>> spliterator() {return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);}public final void forEach(Consumer<? super Map.Entry<K,V>> action) {Node<K,V>[] tab;if (action == null)throw new NullPointerException();if (size > 0 && (tab = table) != null) {int mc = modCount;for (int i = 0; i < tab.length; ++i) {for (Node<K,V> e = tab[i]; e != null; e = e.next)action.accept(e);}if (modCount != mc)throw new ConcurrentModificationException();}} }final class EntryIterator extends HashIteratorimplements Iterator<Map.Entry<K,V>> {public final Map.Entry<K,V> next() { return nextNode(); } }// HashMap自己實現的遍歷方法。上面的所有方法都是圍繞這個類展開的。下面具體講解這個類的實現原理。 abstract class HashIterator {Node<K,V> next; // 指向下一個元素Node<K,V> current; // 指向當前元素int expectedModCount;int index; // 當前元素位置HashIterator() {expectedModCount = modCount;Node<K,V>[] t = table;current = next = null;index = 0;if (t != null && size > 0) { // 找到table中的第一個元素do {} while (index < t.length && (next = t[index++]) == null);}}public final boolean hasNext() {return next != null;}final Node<K,V> nextNode() {Node<K,V>[] t;Node<K,V> e = next;if (modCount != expectedModCount)throw new ConcurrentModificationException();if (e == null)throw new NoSuchElementException();// 判斷當前元素是否為鏈表中的最后一個元素,如果在鏈表尾部,那么就需要重新遍歷table,// 順序找到下元素的位置。if ((next = (current = e).next) == null && (t = table) != null) {do {} while (index < t.length && (next = t[index++]) == null);}return e;}// 刪除當前遍歷的元素。public final void remove() {Node<K,V> p = current;if (p == null)throw new IllegalStateException();if (modCount != expectedModCount)throw new ConcurrentModificationException();current = null;K key = p.key;removeNode(hash(key), key, null, false, false);expectedModCount = modCount;} }總結一下這個遍歷的過程是 EntrySet -> EntryIterator -> HashIterator。同理對key的遍歷過程就是 KeySet -> KeyIterator -> HashIterator。可以看出來不管是哪種遍歷,最終都是調用了HashIterator。
轉載于:https://www.cnblogs.com/hd-zg/p/6929685.html
總結
- 上一篇: 51. N皇后/52. N皇后 II
- 下一篇: 获取CPU利用率 系统内存和进程内存