#HashMap
#####前言: 從今天開始我將介紹Map系列接口,我認(rèn)為Map是集合類中概念最多,實(shí)現(xiàn)最復(fù)雜的一類接口。在講解過程中會涉及到不少數(shù)據(jù)結(jié)構(gòu)的知識,這部分知識點(diǎn)需要讀者額外花一定時(shí)間系統(tǒng)學(xué)習(xí)。
HashMap是Map的一個(gè)實(shí)現(xiàn)類,這個(gè)類很重要,是很多集合類的實(shí)現(xiàn)基礎(chǔ),底層用的就是他,比如前文中講到的HashSet,下文要講到的LinkedHashMap。我們可以將HashMap看成是一個(gè)小型的數(shù)字字典,他以key-value的方式保存數(shù)據(jù),Key全局唯一,并且key和value都允許為null。
HashMap底層是通過維護(hù)一個(gè)數(shù)據(jù)來保存元素。當(dāng)創(chuàng)建HashMap實(shí)例的時(shí)候,會通過指定的數(shù)組大小以及負(fù)載因子等參數(shù)創(chuàng)建一個(gè)空的數(shù)組,當(dāng)在容器中添加元素的時(shí)候,首先會通過hash算法求得key的hash值,再根據(jù)hash值確定元素在數(shù)組中對應(yīng)的位置,最后將元素放入數(shù)組對應(yīng)的位置。在添加元素的過程中會出現(xiàn)hash沖突問題,沖突處理的方法就是判斷key值是否相同,如果相同則表明是同一個(gè)元素,替換value值。如果key值不同,則把當(dāng)前元素添加到鏈表尾部。這里引出了一個(gè)概念,就是HashMap的數(shù)據(jù)結(jié)構(gòu)其實(shí)是:hash表+單向鏈表。通過鏈表的方式把所有沖突元素放在了數(shù)組的同一個(gè)位置。但是當(dāng)鏈表過長的時(shí)候會影響HashMap的存取效率。因此我們在實(shí)際使用HashMap的時(shí)候就需要考慮到這個(gè)問題,那么該如何控制hash沖突的出現(xiàn)頻率呢?HashMap中有一個(gè)負(fù)載因子(loadFactor)的概念。容器中實(shí)際存儲元素的size = loadFactor * 數(shù)組長度,一旦容器元素超出了這個(gè)size,HashMap就會自動(dòng)擴(kuò)容,并對所有元素重新執(zhí)行hash操作,調(diào)整位置。好了說了這么多,下面就開始介紹源碼實(shí)現(xiàn)。
#####一、Node結(jié)構(gòu)介紹 Node類實(shí)現(xiàn)了Map.Entry接口,他是用于存放數(shù)據(jù)的實(shí)體,是容器中存放數(shù)據(jù)的最小單元。Node的數(shù)據(jù)結(jié)構(gòu)是一個(gè)單向鏈表,為什么選用這種結(jié)構(gòu)?那是因前文講到的,HashMap存放數(shù)據(jù)的結(jié)構(gòu)是: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;} }這個(gè)結(jié)構(gòu)非常簡單,定義了一個(gè)hash和key,hash值是對key進(jìn)行hash以后得到的。value保存實(shí)際要存儲的對象。next指向下一個(gè)節(jié)點(diǎn)。當(dāng)hash沖突以后,就會將沖突的元素放入這個(gè)單向鏈表中。
#####二、創(chuàng)建HashMap 創(chuàng)建HashMap實(shí)例有四個(gè)構(gòu)造方法,這里著重介紹一個(gè),看源碼:
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的數(shù)組大小是有講究的,他必須是2的冪,這里通過一個(gè)牛逼哄哄的位運(yùn)算算法,找到大于或等于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; }構(gòu)造方法中有兩個(gè)參數(shù),第一個(gè)initialCapacity定義map的數(shù)組大小,第二個(gè)loadFactor意為負(fù)載因子,他的作用就是當(dāng)容器中存儲的數(shù)據(jù)達(dá)到loadFactor限度以后,就開始擴(kuò)容。如果不設(shè)定這樣參數(shù)的話,loadFactor就等于默認(rèn)值0.75。但是細(xì)心的你會發(fā)現(xiàn),容器創(chuàng)建以后,并沒有創(chuàng)建數(shù)組,原來table是在第一次被使用的時(shí)候才創(chuàng)建的,而這個(gè)時(shí)候threshold = initialCapacity * loadFactor。 這才是這個(gè)容器的真正的負(fù)載能力。
tableSizeFor這個(gè)方法的目的是找到大于或等于initialCapacity的最小的2的冪,這個(gè)算法寫的非常妙,值得我們細(xì)細(xì)品味。
假設(shè)cap=7
第一步 n = cap -1 = 6 = 00000110
第二步 n|= n>>>1:
n>>>1表示無符號右移1位,那么二進(jìn)制表示為00000011,此時(shí)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
其實(shí)他的原理很簡單,第一步先對cap-1是因?yàn)槿绻鹀ap原本就是一個(gè)2的冪,那么最后一步加1,會使得這個(gè)值變成原來的兩倍,但事實(shí)上原來這個(gè)cap就是2的冪,就是我們想要的值。接下來后面的幾步無符號右移操作是把高位的1補(bǔ)到低位,經(jīng)過一系列的位運(yùn)算以后的值必定是000011111...他的低位必定全是1,那么最后一步加1以后,這個(gè)值就會成為一個(gè)00010000...(2的冪次),這就是通過cap找到2的冪的方法。看到如此簡約高效的算法,我服了。
#####三、put添加元素 添加一個(gè)元素是所有容器中的標(biāo)配功能,但是至于添加方式那就各有千秋,Map添加元素的方式是通過put,向容器中存入一個(gè)Key-Value對。下面我將詳細(xì)介紹put的實(shí)現(xiàn)過程,這個(gè)方法非常重要,吃透了這個(gè)方法的實(shí)現(xiàn)原理,基本也就能搞懂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是空的,首先創(chuàng)建一個(gè)指定大小的table。if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 通過對hash與數(shù)組長度的與操作,確定key對應(yīng)的數(shù)組位置,然后讀取該位置中的元素。if ((p = tab[i = (n - 1) & hash]) == null)// 如果當(dāng)前位置為空,那么就在當(dāng)前數(shù)組位置,為這個(gè)key-value創(chuàng)建一個(gè)節(jié)點(diǎn)。tab[i] = newNode(hash, key, value, null);else {// 如果當(dāng)前位置已經(jīng)存在元素,那么就要逐個(gè)讀取這條鏈表的元素。Node<K,V> e; K k;// 如果key和hash值都等于當(dāng)前頭元素,那么這存放的兩個(gè)元素是相同的。if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果當(dāng)前位置的鏈表類型是TreeNode,那么就講當(dāng)前元素以紅黑樹的形式存放。else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {// 遍歷鏈表的所有元素,如果都未找到相同key的元素,那么說明這個(gè)元素并不在容器中存在,因此將他添加到鏈表尾部,并結(jié)束遍歷。if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 如果在遍歷過程中,發(fā)現(xiàn)了相同的key值,那么就結(jié)束遍歷。if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 如果e != null 說明在當(dāng)前容器中,存在一個(gè)相同的key值,那么就要替換key所對應(yīng)的valueif (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;// 這是專門留給LinkedHashMap調(diào)用的回調(diào)函數(shù),LinkedHashMap會實(shí)現(xiàn)這個(gè)方法。從這里可以看出,HashMap充分的考慮了他的擴(kuò)展性。afterNodeAccess(e);return oldValue;}}++modCount;// 這里判斷當(dāng)前元素的數(shù)量是否超過了容量的上限,如果超過了,就要重新進(jìn)行擴(kuò)容,并對當(dāng)前元素重新hash,所以再次擴(kuò)容以后的元素位置都是會改變的。if (++size > threshold)resize();// 此方法也是HashMap留給LinkedHashMap實(shí)現(xiàn)的回調(diào)方法。透露一下,因?yàn)長inkedHashMap在插入元素以后,都會維護(hù)他的一個(gè)雙向鏈表afterNodeInsertion(evict);return null; }#####四、get獲取元素 使用HashMap有一個(gè)明顯的優(yōu)點(diǎn),就是他的存取時(shí)間開銷基本維持在O(1),除非在數(shù)據(jù)量大了以后hash沖突的元素多了以后,對其性能有一定的影響。那么現(xiàn)在介紹的get方法很好的體現(xiàn)了這個(gè)優(yōu)勢。
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value; }// 同一個(gè)key的hash值是相同的,通過hash就可以求出數(shù)組的下標(biāo),便可以在O(1)的時(shí)間內(nèi)獲取元素。 final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 在容器不為空,并且對應(yīng)位置也存在元素的情況下,那么就要遍歷鏈表,找到相同key值的元素。if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 如果第一個(gè)元素的key值相同,那么這個(gè)元素就是我們要找的。if (first.hash == hash &&((k = first.key) == key || (key != null && key.equals(k))))return first;// 如果第一個(gè)元素不是我們要找的,接下來就遍歷鏈表元素,如果遍歷完了以后都沒找到,說明不存在這個(gè)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刪除元素 刪除元素的實(shí)現(xiàn)原理和put,get都類似。remove通過給定的key值,找到在hash表中對應(yīng)的位置,然后找出相同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值定位元素位置,并對其刪除。這里的實(shí)現(xiàn)和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參數(shù),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動(dòng)態(tài)擴(kuò)容 resize這個(gè)方法非常重要,他在添加元素的時(shí)候就會被調(diào)用到。resize的目的是在容器的容量達(dá)到上限的時(shí)候,對其擴(kuò)容,使得元素可以繼續(xù)被添加進(jìn)來。這里需要關(guān)注兩個(gè)參數(shù)threshold和loadFactor,threshold表示容量的上限,當(dāng)容器中元素?cái)?shù)量大于threshold的時(shí)候,就要擴(kuò)容,并且每次擴(kuò)容都是原來的兩倍。loadFactor表示hash表的數(shù)組大小。這兩個(gè)參數(shù)的配合使用可以有效的控制hash沖突數(shù)量。
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 如果容器并不是第一次擴(kuò)容的話,那么oldCap必定會大于0if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// threshold和數(shù)組大小cap共同擴(kuò)大為原來的兩倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1;}// 第一次擴(kuò)容,并且設(shè)定了threshold值。else if (oldThr > 0)newCap = oldThr;else {// 如果在創(chuàng)建的時(shí)候并沒有設(shè)置threshold值,那就用默認(rèn)值newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {// 第一次擴(kuò)容的時(shí)候threshold = cap * loadFactorfloat ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;// 創(chuàng)建數(shù)組@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 如果不是第一次擴(kuò)容,那么hash表中必然存在數(shù)據(jù),需要將這些數(shù)據(jù)重新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)// 重新計(jì)算在數(shù)組中的位置。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;// 因?yàn)閏ap都是2的冪次,假設(shè)oldCap == 10000,// 假設(shè)e.hash= 01010 那么 e.hash & oldCap == 0。// 老位置= e.hash & oldCap-1 = 01010 & 01111 = 01010// newCap此時(shí)為100000,newCap-1=011111。// 此時(shí)e.hash & newCap 任然等于01010,位置不變。// 如果e.hash 假設(shè)為11010,那么 e.hash & oldCap != 0// 原來的位置為 e.hash & oldCap-1 = 01010// 新位置 e.hash & newCap-1 = 11010 & 011111 = 11010// 此時(shí) 新位置 != 老位置 新位置=老位置+oldCap// 因此這里分類兩個(gè)索引的鏈表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的封裝,三種實(shí)現(xiàn)方式大同小異,因此我著重介紹EntryIterator的實(shí)現(xiàn)。
// 對HashMap元素進(jìn)行遍歷。 public Set<Map.Entry<K,V>> entrySet() {Set<Map.Entry<K,V>> es;// 第一次遍歷的時(shí)候,實(shí)例化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自己實(shí)現(xiàn)的遍歷方法。上面的所有方法都是圍繞這個(gè)類展開的。下面具體講解這個(gè)類的實(shí)現(xiàn)原理。 abstract class HashIterator {Node<K,V> next; // 指向下一個(gè)元素Node<K,V> current; // 指向當(dāng)前元素int expectedModCount;int index; // 當(dāng)前元素位置HashIterator() {expectedModCount = modCount;Node<K,V>[] t = table;current = next = null;index = 0;if (t != null && size > 0) { // 找到table中的第一個(gè)元素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();// 判斷當(dāng)前元素是否為鏈表中的最后一個(gè)元素,如果在鏈表尾部,那么就需要重新遍歷table,// 順序找到下元素的位置。if ((next = (current = e).next) == null && (t = table) != null) {do {} while (index < t.length && (next = t[index++]) == null);}return e;}// 刪除當(dāng)前遍歷的元素。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;} }總結(jié)一下這個(gè)遍歷的過程是 EntrySet -> EntryIterator -> HashIterator。同理對key的遍歷過程就是 KeySet -> KeyIterator -> HashIterator。可以看出來不管是哪種遍歷,最終都是調(diào)用了HashIterator。
轉(zhuǎn)載于:https://www.cnblogs.com/hd-zg/p/6929685.html
總結(jié)
- 上一篇: 51. N皇后/52. N皇后 II
- 下一篇: 世界是公平的