類聲明html
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
AbstractMap
抽象類。Map的一些操做這裏面已經提供了默認實現,後面具體的子類若是沒有特殊行爲,可直接使用AbstractMap
提供的實現。Map
,Clone
,Serializable
接口。支持拷貝和序列化。支持Map常見的增刪查改。HashMap
是數組和鏈表的折中,既保證了幾乎$O(1)$的時間複雜度,也保證了插入和刪除的時間複雜度爲$O(1)$。在HashMap
內部,採用了數組+鏈表的形式來組織鍵值對Entry <Key,Value>
。java
HashMap
內部維護了一個Entry[] table
數組,當咱們使用 new HashMap()建立一個HashMap時,Entry[] table
的默認長度爲16。Entry[] table
的長度又被稱爲這個HashMap
的容量(capacity
);node
對於Entry[] table
的每個元素而言,或爲null
,或爲由若干個Entry<Key,Value>
組成的鏈表。HashMap中Entry<Key,Value>
的數目被稱爲HashMap的大小(size
);算法
Entry[] table
中的某一個元素及其對應的Entry<Key,Value>
又被稱爲桶(bucket
);數組
HashMap的容量(即Entry[] table
的大小)*加載因子(經驗值0.75)就是threshhold
,當hashmap的size大於threshhold時,容量翻倍。安全
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
爲何須要將key的hashcode的高16爲與第16爲異或? 充分利用key的高位和低位(否則在利用hash求index的時候可能永遠也利用不上key的高位,主要是table的長度n的二進制高位都是0,在求 (n-1)&hash
是利用不上key的hash的高位的),以最小的代價來下降衝突的可能性。 原話:we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.數據結構
Key
的hashCode
,能夠直接定位到存儲這個Entry<Key,Value>
的桶所在的位置,這個時間的複雜度爲O(1);Entry<Key,Value>
對象節點,須要遍歷這個桶的Entry<Key,Value>
鏈表,時間複雜度爲O(n);或者遍歷紅黑樹,時間複雜度爲O(logn); 那麼,如今,咱們應該儘量地將第2個問題的時間複雜度O(n)降到最低,咱們應該要求**桶中的鏈表的長度越短越好!**桶中鏈表的長度越短,所消耗的查找時間就越低,最好就是一個桶中就一個Entry<Key,Value>
對象節點就行了!這樣一來,桶中的Entry<Key,Value>
對象節點要求儘量第少,這就要求,HashMap中的桶的數量要多了。多線程
HashMap的桶數目,即Entry[]table
數組的長度,因爲數組是內存中連續的存儲單元,它的空間代價是很大的,可是它的隨機存取的速度是Java集合中最快的。咱們增大桶的數量,而減小Entry<Key,Value>
鏈表的長度,來提升從HashMap
中讀取數據的速度。這是典型的拿空間換時間的策略。app
可是咱們不能剛開始就給HashMap分配過多的桶(即Entry[] table
數組起始不能太大),這是由於數組是連續的內存空間,它的建立代價很大,何況咱們不能肯定給HashMap分配這麼大的空間,它實際到底可以用多少,爲了解決這一個問題,HashMap採用了根據實際的狀況,動態地分配桶的數量。函數
動態分配桶的數量,HashMap動態分配桶的數量的策略: 若是 HashMap的大小 > HashMap的容量(即Entry[] table
的大小)*加載因子(經驗值0.75) 則 HashMap中的Entry[]table
的容量擴充爲當前的一倍;而後從新將之前桶中的Entry<Key,Value>
鏈表從新分配到各個桶中。
容量翻倍,怎麼從新分配解決hash衝突?:容量翻倍後,從新計算每一個Entry<Key,Value>
的index,將有限的元素映射到更大的數組中,減小hash衝突的機率。
你瞭解從新調整HashMap大小存在什麼問題嗎?:多線程的狀況下,可能產生條件競爭(race condition)(雖然通常咱們不使用HashMap在多線程環境中)。若是在多線程環境中使用HashMap,若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。
//默認的初始容量,必須是2的冪。 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換) static final int MAXIMUM_CAPACITY = 1 << 30; //默認裝載因子,這個後面會作解釋 static final float DEFAULT_LOAD_FACTOR = 0.75f; //JDK1.8特有 //當hash值相同的記錄超過TREEIFY_THRESHOLD,會動態的使用一個專門的treemap實現來代替鏈表結構,使得查找時間複雜度從O(n)變爲O(logn) static final int TREEIFY_THRESHOLD = 8; //JDK1.8特有 //也是閾值同上一個相反,當桶(bucket)上的鏈表數小於UNTREEIFY_THRESHOLD 時樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6; //JDK1.8特有 //樹的最小的容量,至少是 4 x TREEIFY_THRESHOLD = 32 而後爲了不(resizing 和 treeification thresholds) 設置成64 static final int MIN_TREEIFY_CAPACITY = 64; //存儲數據的Entry數組,長度是2的冪。看到數組的內容了,接着看數組中存的內容就明白爲何博文開頭先複習數據結構了 transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; //map中保存的鍵值對的數量 transient int size; //Map結構被改變的次數 transient int modCount; //須要調整大小的極限值(容量*裝載因子)。保存的是下次entrySet大小的極限值。 int threshold; //裝載因子,當Map結構中的bucket數等於capacity*loadFactor時,bucket數量翻倍。 final float loadFactor;
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } 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); } public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
有四個構造器,除HashMap(int initialCapacity, float loadFactor)
都是使用默認的加載因子構造。 HashMap(int initialCapacity, float loadFactor)
中,加載因子是用戶設置的,而且根據用戶設置的加載因子和容量肯定threshold。 肯定threshold的方法是tableSizeFor
,保證threshhold
是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; }
先將cap-1保證最後的結果是大雨或等於cap的最小的2的冪次方,例如輸入的原本就是一個2的冪次方的數,好比4,若是不先-1,則會輸出8,-1就會輸出4。 爲何每次移動位數的分別是1,2,4,8,16位?先移動一位,並作或運算,將最高位上的二進制1
移動到次高位;再右移兩位,將最高位和次高位上的二進制11
移動到與次高位相鄰的兩位上,以此類推,最後保證最改成和比最高位的全部二進制位所有是1,在返回時,+1,就保證這個書是2的冪次方。 爲何沒有移動32位?正整數的最大2的冪次方是$2^16$
次方。
tableSizeFor
是一個求大於或等於給定數的最小2的冪次方的最快方法。實用的算法!
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; } }
繼承自Map.Entry
,主要功能:節點的初始化,set方法,重寫hashCode和equals方法。是全部操做的基礎
put
public V put(K key, V value) { //傳入key的hash值 return putVal(hash(key), key, value, false, true); } /** * hash key的hash值 * key 鍵 * value 值 * onlyIfAbsent true時,不改變已經存在的值 * evict false時,table在建立模式中 */ 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爲空則建立table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 計算index,當index所在bucket沒有數據null,則直接將index位置設置爲傳入的key-value。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 節點存在,而且key值相等,直接覆蓋 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); //節點中的數據不是TreeNode的實例,是普通的單鏈表結構 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 1st 超過TREEIFY_THRESHOLD,則將鏈表變爲樹結構,提升衝突鏈效率 treeifyBin(tab, hash); break; } //若是找到key,後面直接覆蓋 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // 找到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; }
put函數大體的思路爲:
resize
。resize
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //探測:容量翻倍後仍是小於MAXIMUM_CAPACITY,而且原來的容量大於等於默認容量。則threshold翻倍,容量翻倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 初始化的容量被加入到threshold中,則新的容量等於就得threshold newCap = oldThr; else { // threshold=0,即threshold未被使用過。 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) {//代表 float 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; 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; } } } } } return newTab; }
(e.hash & oldCap) == 0
是擴容的關鍵點,由於容量擴展爲原來的兩倍,至關於oldCap<<1
,因此計算hash時,須要考慮的二進制位數向高位多增長了一位(至關於求hash的掩碼由之前的前x位爲0,後32-x位1變爲前x-1位0,32-x+1位1),爲了不重複計算hash(key)和(n-1)&hash
,直接判斷key的hash在增長位上的值是否爲1(經過e.hash & oldCap
,獲得增長位上,key的hash值。),若是爲1,索引的二進制位的增長位也爲1,若是爲0,則索引的增長位也是0。既省去了從新計算hash值的時間,並且同時,因爲新增的1bit是0仍是1能夠認爲是隨機的,所以resize的過程,均勻的把以前的衝突的節點分散到新的bucket。 例如: 其中增長位爲紅色。 通過擴容從新分配 ,原來在一個bucket的index 5,分配到不一樣的index=21的bucket,避免與index=5的key衝突,提升了查詢的效率。
resize的策略:
putMapEntries
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
putMapEntries是一個默認訪問權限的final類型函數,表示該函數只能在它所在的包內訪問,而且該方法不能被重載。
java訪問權限複習: java的訪問權限有:public,protected,private,默認。
- public是公開訪問,全部的包中的類都可訪問;
- protected是繼承訪問,對於同一個包的類,這個類的方法或變量是能夠被訪問的;對於不一樣包的類,只有繼承於該類的類才能夠訪問到該類的方法或者變量;
- private只能在該類自己中被訪問,在類外以及其餘類中都不能顯示地進行訪問;
- 默認訪問權限是包訪問權限,只有本包內的類能夠訪問。
get
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; }
getNode的核心流程:
final關鍵字:
- 修飾變量:變量的引用不能變,可是能夠改變引用值;成員變量必須在構造器中初始化;
- 修飾函數:把方法鎖定,以防任何繼承類修改它的含義;提升效率效率。在早期的Java實現版本中,會將final方法轉爲內嵌調用。可是若是方法過於龐大,可能看不到內嵌調用帶來的任何性能提高。在最近的Java版本中,不須要使用final方法進行這些優化了。
- 修飾類:類不能被繼承。
null
的形式存儲<null,Value>
鍵值對;hashCode()
進行hashing,並計算下標( n-1 )& hash
,從而得到buckets的位置。若是產生碰撞,則利用key.equals()
方法去鏈表或樹中去查找對應的節點。TREEIFY_THRESHOLD
閥值,當一個桶裏的Entry超過閥值,就不以單向鏈表而以紅黑樹來存放以加快Key的查找速度。Thanks for reading! want more