目錄html
@(HashMap源碼刨析)java
JDK1.7:數組+鏈表node
JDK1.8:數組+鏈表+紅黑樹程序員
前五個問題環境用的是是JDK1.7,後面所有是1.8算法
簡單的說是個「擾動函數」,目的是爲了使散列分佈的更加均勻。數組
具體算法是用key的Hashcode值右移16位,將hashcode高位和低位的值進行混合作異或運算,低位的信息中加入了高位的信息,這樣高位的信息被變相的保留了下來。摻雜的元素多了,那麼生成的hash值的隨機性會增大,獲得Hash。最後與table長度進行與運算(indexFor()方法),和取餘是一個結果,不過與運算更加節省計算機資源。
這裏用&運算的原理:n必定是2的次方數(由擴容機制決定),n-1的二進制表示則全爲1,而&運算的方式是雙方爲1結果才爲1,那麼無論hash有多大,結果都取決於n-1的這幾位,大於n-1的那部分全補爲0,則不可能越界。安全
在多線程狀況下進行擴容容易造成環形鏈表,關鍵點在於resieze()方法中的transfer()方法。數據結構
在單線程下代碼執行過程:多線程
在多線程下代碼執行過程:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FNLaBmyb-1579436677465)(file:///C:\Users\李正陽\AppData\Local\Temp\ksohtml17192\wps3.png)]app
當一個線程執行完Rehash完以後另外一個再在舊map中Rehash,因爲鏈表已經逆序,因此next會指回去,再進行Rehash就會造成環形鏈表
(1) JDK1.7使用的是頭插法,1.8以後是尾插法。其緣由在於1.7是用單鏈表進行的縱向延伸,當採用頭插法能提升插入的效率(由於加到尾部還須要遍歷鏈表),可是容易出現逆序和環形鏈表死循環的問題。在1.8以後是由於加入了紅黑樹使用尾插法(尾插法要遍歷鏈表,順便判斷鏈表長度是否大於8),可以避免逆序和鏈表死循環問題。紅黑樹能提升查找效率,比鏈表的查找效率高。
(2) 擴容後數據儲存的計算方式不同
JDK1.7:直接用hash值和須要擴容的二進制數進行&(這裏就是爲何擴容的時候爲啥必定必須是2的多少次冪的緣由所在,由於若是隻有2的n次冪的狀況時最後一位二進制數才必定是1,這樣能最大程度減小hash碰撞)(hash值 & length-1)。
JDK1.8:直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而再也不是JDK1.7的那種異或的方法。可是這種方式就至關於只須要判斷Hash值的新增參與運算的位是0仍是1就直接迅速計算出了擴容後的儲存方式。(table變爲2倍,則左邊增長一位1,和Hash值進行與操做便可)
(3) JDK1.7使用的是數組+單鏈表的數據結構。JDK1.8及之後使用的是數組+鏈表+紅黑樹的數據結構(當鏈表長度到達8的時候,也就是默認閾值,會自動擴容把鏈表轉化成紅黑樹的數據結構)
(1) HashMap是非線程安全的,而且能夠儲存NULL。HashTbale是線程安全(即synchronized),但不能存儲NULL。
(2) HashMap利用HashCode從新計算Hash值,HashTbale直接使用key的HashCode(),再取模算下標。
(3) 內部實現使用的數組初始化和擴容方式不一樣。HashTable在不指定容量的狀況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量必定要爲2的整數次冪,而HashMap則要求必定爲2的整數次冪。Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍。
核心數據如 value ,以及鏈表都是 volatile 修飾的,保證了獲取時的可見性。
在HashMap中,默認建立的數組長度是16,也就是哈希桶個數爲16,當添加key-value的時候,會先計算出他們的哈希值(h = hash),而後用return h & (length-1)
就能夠算出一個數組下標,這個數組下標就是鍵值對應該存放的位置。
可是,當數據較多的時候,不一樣鍵值對算出來的hash值相同,而致使最終存放的位置相同,這就是hash衝突,當出現hash衝突的時候,該位置的數據會轉變成鏈表的形式存儲,可是咱們知道,數組的存儲空間是連續的,因此能夠直接使用下標索引來查取,修改,刪除數據等操做,並且效率很高。而鏈表的存儲空間不是連續的,因此不能使用下標 索引,對每個數據的操做都要進行從頭至尾的遍歷,這樣會使效率變得很低,特別是當鏈表長度較大的時候。爲了防止鏈表長度較大,須要對數組進行動態擴容。
數組擴容須要申請新的內存空間,而後把以前的數據進行遷移,擴容頻繁,須要耗費較多時間,效率下降,若是在使用完一半的時候擴容,空間利用率就很低,若是等快滿了再進行擴容,hash衝突的機率增大!!那麼何時開始擴容呢???
爲了平衡空間利用率和hash衝突(效率),設置了一個加載因子(loadFactor
),而且設置一個擴容臨界值(threshold = DEFAULT_INITIAL_CAPACITY * loadFactor
),就是說當使用了16*0.75=12個數組之後,就會進行擴容,且變爲原來的兩倍
在理想狀況下,使用隨機哈希嗎,節點出現的頻率在hash桶中遵循泊松分佈,同時給出了桶中元素的個數和機率的對照表。
從上表能夠看出當桶中元素到達8個的時候,機率已經變得很是小,也就是說用0.75做爲負載因子,每一個碰撞位置的鏈表長度超過8個是幾乎不可能的。
hash容器指定初始容量儘可能爲2的冪次方。
HashMap負載因子爲0.75是空間和時間成本的一種折中。
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. * * 構造函數,設置基本的加載因子爲0.75,意思是當一個 * 表的長度超過 * 臨界值就會再散列而後放回容器,這是十分耗時間的。 * 這個臨界值由負載因子和容量大小來決定,而且咱們能夠 * 手動初始化這個值 * */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
用戶輸入的容量初始值和負載因子後賦值檢查
public HashMap(int initialCapacity, float loadFactor) { //初始化數組默認值小於0直接拋出 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //大於最大值就直接默認爲最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //負載因子小於0, Float.isNaN或者輸入的不是一個數字拋出異常 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //賦值操做 this.loadFactor = loadFactor; //確保你賦值雖然不是2的k次方,也會輸出2的k次方 this.threshold = tableSizeFor(initialCapacity); }
數組的初始默認值:
/** * * 數組的默認初始值爲16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
爲何最大容量是這麼大?
int 是32爲整數,四個字節,負數爲1
1 << 30 = 1073741824
1 << 31 = -2147483648
1 << 32 = 1
1 << 33 = 2
1 << -1 = -2147483648
首位爲符號位,正數是0,負數爲1
31位存儲的是int型的補碼,因此最大隻能30位
若是我要存的值大於2^30如何處理
有一個resize()方法,這個方法的做用就是當使用的容量到達threshold容量的時候擴容
//可是若是最大容量大於默認的最大容量,會使threshold擴充爲 Integer.MAX_VALUE if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; }
threshold
int threshold;
threshold = 初始容量 * 加載因子至關於擴容的限制值,至關於實際使用量
能夠擴充到Integer.MAX_VALUE,仍是爲了能繼續存儲,由於到2 << 30 就會溢出。
代表不進行擴容了
因此說HashMap的總容量天然是MAXIMUM_CAPACITY
同時這個值沒有在建立的時候初始化,而是在put方法中初始化了。
table
transient Node<K,V>[] table;
是一個數組單鏈表結構
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next;
初始化容量,找到離輸入最近2的冪,由於HashMap要求容量必須是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; }
int n = cap - 1是爲了防止cap已是2的冪了,一下舉一個例子:
cap = 11
n |= >>> 1:
0000 1011 | 0000 0101 = 0000 1111
n |= >>> 2;
0000 1111 | 0000 0011 = 0000 1111
繼續向下推,也是同樣結果
若是最後值爲32個1天然取到最大值MAXIMUM_CAPACITY,若是不是就給n+1,那麼此時n = 16
put函數不是具體實現,主要是爲了方便用戶,就像工廠方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
調用的putVal函數
putVal(hash(key), key, value, false, true); final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
一共有五個參數,第一個是插入元素的key的hash值,第二個是key自己,第三個是value,onlyIfAbsent true 表明映射存在不替換原值,evict 若是位false就表明HahMap表明正處於建立階段
putVal方法中,衝突以後判斷是否是處於數組的第一位
//肯定是p這個位置hash值相同,而且key的值也相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //臨時結點e = p e = p;
定理:
equal objects must have equal hash codes.
首先:java.lang.Object.hashCode() 是三條約定是
一、屢次運行 hashCode(),其值必須老是一致的(前提:一、 equals() 中用到的信息沒發生變化 二、在同一次 execution 中)
二、obj1.equals(obj2) == true,則必須 obj1.hashCode() == obj1.hashCode() 老是 true
三、obj1.equals(obj2) == false,則 obj1.hashCode() == obj2.hashCode() 最好 false 這是由於 HashMap.containsKey(),HashMap.put() 時
a:因爲 hash 不一樣,則直接就不嘗試了(好。這樣效率高啊)
b:「兩把刷子程序員」 把 hash 弄成相同的(equals()不一樣,hashCode()相同),還得向下嘗試 equals() (很差)
狀況一:
出現hash衝突,同時和數組指定位置第一個元素是同樣的
代碼節選:
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //臨時結點e = p e = p; .............................................. //若是是第一種狀況就,e的值位數組中第一個 if (e != null) { // existing mapping for key //保存結點e中的值 V oldValue = e.value; //若是oldValue(如今在數組中的結點值)或者onlyIfAbsent的值爲false if (!onlyIfAbsent || oldValue == null) //覆蓋現有結點的值 e.value = value; //給LinkedHashMap預留的方法位 afterNodeAccess(e); //返回舊的值 return oldValue; }
狀況二:發現插入位置已是紅黑樹了,返回紅黑樹的結點
//第二種狀況若是是紅黑樹就按照紅黑樹的插入結點的方式 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); ............................................... //若是是第一種狀況就,e的值位數組中第一個,第三種狀況也要執行接下來的代碼,第二種狀況也會執行 if (e != null) { // existing mapping for key //保存結點e中的值 V oldValue = e.value; //若是oldValue(如今在數組中的結點值)或者onlyIfAbsent的值爲false if (!onlyIfAbsent || oldValue == null) //覆蓋現有結點的值 e.value = value; //給LinkedHashMap預留的方法位 afterNodeAccess(e); //返回舊的值 return oldValue; }
狀況3:雖然有衝突可是 不是第一個,遍歷數組以後,找到就替換,沒找到就插入,插入以後大於8執行桶的樹型化
else { //衝突的第三種狀況,不是第一個久開始遍歷 for (int binCount = 0; ; ++binCount) { //若是已經到達了鏈表的尾端 if ((e = p.next) == null) { //鏈表的末端插入當前須要插入的值 p.next = newNode(hash, key, value, null); //若是鏈表長度大於等於7,由於是從0開始的,因此是八個長度 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //就將這個位置的鏈表紅黑樹化,主要是爲了提升後續鏈表的查找效率 treeifyBin(tab, hash); //天然到達鏈尾末端要結束循環 break; } //若是在鏈表中找到了與插入相同的元素就直接結束循環,而後執行後面替換 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } .................................................... //若是是第一種狀況就,e的值位數組中第一個,第三種狀況也要執行接下來的代碼,第二種狀況也會執行 if (e != null) { // existing mapping for key //保存結點e中的值 V oldValue = e.value; //若是oldValue(如今在數組中的結點值)或者onlyIfAbsent的值爲false if (!onlyIfAbsent || oldValue == null) //覆蓋現有結點的值 e.value = value; //給LinkedHashMap預留的方法位 afterNodeAccess(e); //返回舊的值 return oldValue; }
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,若是超過,進行擴容。
put方法的完整代碼:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //若是數組爲空,因爲建立的時候沒有初始化,看resize()作了什麼操做 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //檢查數組的這個位置是否是已經有了元素,p爲這個位置的元素 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //已經有了元素執行這部份內容 Node<K,V> e; K k; //衝突的第一種狀況肯定是p這個位置第一個hash值相同,而且key的equals值也相同,若是hash值不相等就不用繼續運行了 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //臨時結點e = p 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); //若是鏈表長度大於等於7,由於是從0開始的,因此是八個長度 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //就將這個位置的鏈表紅黑樹化,主要是爲了提升後續鏈表的查找效率 treeifyBin(tab, hash); //天然到達鏈尾末端要結束循環 break; } //若是在鏈表中找到了與插入相同的元素 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //若是是第一種狀況就,e的值位數組中第一個,第三種狀況也要執行接下來的代碼 if (e != null) { // existing mapping for key //保存結點e中的值 V oldValue = e.value; //若是oldValue(如今在數組中的結點值)或者onlyIfAbsent的值爲false if (!onlyIfAbsent || oldValue == null) //覆蓋現有結點的值 e.value = value; //給LinkedHashMap預留的方法位 afterNodeAccess(e); //返回舊的值 return oldValue; } } //修改計數增長 ++modCount; //添加結點以後檢查時候已經到達了擴容界限 if (++size > threshold) //擴容 resize(); //爲linkedHashMap服務 afterNodeInsertion(evict); return null; }
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //若是數組爲空,就將0賦值給oldCap,不爲空則返回,表的大小 int oldCap = (oldTab == null) ? 0 : oldTab.length; //以前的擴容界限,初始化的時候oldThr不會是0,由於有tableSizeFor()方法,確保oldThr至少是1 int oldThr = threshold; //新的容量和新的擴容界限 int newCap, newThr = 0; //若是是已經初始化的數組,而且數組裏面還有元素,就會直接進入這個分支 if (oldCap > 0) { //可是若是最大容量大於默認的最大容量,會使threshold擴充爲nteger.MAX_VALUE,代表不在進行擴容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; //直接返回舊的表 return oldTab; } //新的容量爲舊容量的2倍,這是向左移一位,因爲原本就是2的冪次,向左移動天然是2倍,而且新容量要小於最大值,舊容量要大於初始值16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //新的限制也要變成原來的兩倍 newThr = oldThr << 1; // double threshold } //這個分支表明的是建立map使用的是帶參構造函數,初始容量不管是輸入多少,都會返回2 ^n,同時這個值存在threshold 中 else if (oldThr > 0) // initial capacity was placed in threshold //給新的容量賦值 newCap = oldThr; else { // zero initial threshold signifies using defaults //這是第一次初始化新的容量,而且調用的是無參構造函數,新的newCap爲16,新的擴容界限爲12 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //這是第一次初始化擴容限制,新的擴容限制爲16 * 0.75 = 12 if (newThr == 0) { float ft = (float)newCap * loadFactor; //若是容量已經大於MAXIMUM_CAPACITY,就給賦值爲Integer.MAX_VALUE newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //將新的擴容界限給threshold threshold = newThr; //初始化數組 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //將newtable賦值給table table = newTab; //若是這個表不爲空的時候 if (oldTab != null) { //遍歷一遍表 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //若是j這個位置的元素不爲null if ((e = oldTab[j]) != null) { //先賦值爲null oldTab[j] = null; //若是e.next爲null就表明的是數組之中有值,且只有一個,直接賦值就行 if (e.next == null) //從新計算hash以後,向新表中直接插入e 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; //爲0走這個分支 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //第一次進入這個循環通常會走這個分支若是不爲0走這個分支 else { if (hiTail == null) //而後hiHead獲得值,至關於初始化鏈表,頭節點和尾結點同樣 hiHead = e; else hiTail.next = e; //hiTail也會獲得值 hiTail = e; } } while ((e = next) != null); //計算出hash和原容量爲0才走這個分支 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //當不爲0時候走這一點,將新鏈表連接到新的座標底下 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } //放回新的數組 return newTab; }
當鏈表的結點數大於8,就將這個結點轉化爲紅黑樹
擴容進行到最後,發現數組不爲空,而且循環遍歷的時候發現這個位置不是單單數組中一個值,還有一個單鏈表這個時候爲何要e.hash & oldCap?不該該是e.hash & newCap
do { next = e.next; if ((e.hash & oldCap) == 0) {
這個是e.hash & oldCap != 0的狀況舉個例子:
擴容以前的容量 :0001 0000
n - 1 0000 11111
新容量: 0010 0000
n - 1: 00001 1111
k1-hash: 0001 0100
與原容量& 0001 0000
newTab[j + oldCap] = hiHead;
原下標: 0000 0100
原下標加原容量 0001 0100
K1-hash與新的n - 1& 0001 0100
這個結果和原下表加原容量的結果是同樣的
e.hash & oldCap 等於0的狀況
K2-hash: 0000 0100
與原容量n-1& 0000 0100
計算出來:新下標和原下標是同樣的,下面是計算與新容量n-1計算
K2-HASH : 0000 0100
n-1 0001 1111
& 0000 0100
與原容量n-1和新容量n-1&其實結果是同樣的
這些只是爲了證實,擴容中,鏈表中的不少元素的新數組下標有兩種可能,一種是還在元素數組下標,還有一種就是元素組加舊的容量的位置
爲何能夠這樣,由於在兩種狀況中,計算他們所處位置其實直接和新容量n-1&是同樣的,上面的兩個例子分別爲兩種狀況,也證實了這一點。
static final int hash(Object key) { int h; //若是輸入的鍵是null,hash就爲0,不然計算hashcode return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
計算出hashcode的值而後和 h >>> 16位的值異或這個是爲何?
(h = key.hashCode()) ^ (h >>> 16),爲何須要異或?
例子:
原值1: 10010001 10010101 10110000 11110001
右移動16位: 00000000 00000000 10010001 10010101
異或: 10010001 10010101 00100001 01100100
進入put函數中比較代碼段
數組大小: 00000000 00000001 00000000 00000000
n - 1: 00000000 00000000 11111111 11111111
原值1與後: 00000000 00000000 10110000 11110001
異或後再與: 00000000 00000000 00100001 01100100
目前看不出什麼,再來看一個原值2與原值1只差第一位
原值2: 00010001 10010101 10110000 11110001
右移動16位: 00000000 00000000 00010001 10010101
異或: 00010001 10010101 10100001 01100100
原值2與後: 00000000 00000000 10110000 11110001
異或後再與: 00000000 00000000 10100001 01100100
可見若是不先異或直接與兩個數相差不大等狀況下的,與以後的狀況是同樣的,若是先進行異或就能夠提升hash值得散列度,能夠避免衝突。
其實(n - 1) & hash 和 value % n 是相等的,可是須要n爲2的冪,同時計算機更加習慣用 & 運算這種而不是這種取餘運算,能夠加快計算機計算的速度。
舉個例子:(只有當n = 2的冪次的時候,才和value % n 相同
n = 16
0000 1111 n - 1
0000 0001 hash
-> 0000 0001
1 % 16 = 1
0000 1111
0000 0101
-> 0000 0101 = 8
n = 15
0000 1110 n -1
0000 0001 hash
-> 0000 0000
同時也會致使hash衝突增長
afterNodeAccess實現方法是LinkedHashMap類中的方法
LinkedHashMap和HashMap的區別看下一個問題
HashMap.afterNodeAccess()中說道,「是爲LinkedHashMap留的後路」。現在行至於此,當觀賞一方。首先須要瞭解的是LinkedHashMap相比HashMap多了有序性,由雙向鏈表(before,after)實現。源碼出現了一些全局變量:
accessOrder:true:按訪問順序排序(LRU),false:按插入順序排序;
head、tail:存放鏈表首尾;
可見僅有accessOrder爲true時,且訪問節點不等於尾節點時,該方法纔有意義。經過before、after重定向,將新訪問節點連接爲鏈表尾節點。
這些方法都是爲了實現LinkedHashMap類的記錄的插入順序
通常狀況下,咱們用的最多的是HashMap,在Map 中插入、刪除和定位元素,HashMap 是最好的選擇。但若是您要按天然順序或自定義順序遍歷鍵,那麼TreeMap會更好。若是須要輸出的順序和輸入的相同,那麼用LinkedHashMap 能夠實現,它還能夠按讀取順序來排列.
HashMap是一個最經常使用的Map,它根據鍵的hashCode值存儲數據,根據鍵能夠直接獲取它的值,具備很快的訪問速度。HashMap最多隻容許一條記錄的鍵爲NULL,容許多條記錄的值爲NULL。
HashMap不支持線程同步,即任一時刻能夠有多個線程同時寫HashMap,可能會致使數據的不一致性。若是須要同步,能夠用Collections的synchronizedMap方法使HashMap具備同步的能力。
Hashtable與HashMap相似,不一樣的是:它不容許記錄的鍵或者值爲空;它支持線程的同步,即任一時刻只有一個線程能寫Hashtable,所以也致使了Hashtable在寫入時會比較慢。
LinkedHashMap保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先獲得的記錄確定是先插入的。
在遍歷的時候會比HashMap慢TreeMap可以把它保存的記錄根據鍵排序,默認是按升序排序,也能夠指定排序的比較器。當用Iterator遍歷TreeMap時,獲得的記錄是排過序的。
***TREEIFY_THRESHOLD***** = 8;
當鏈表長度大於此值時,將鏈表轉化爲紅黑樹。
***UNTREEIFY_THRESHOLD***** = 6;
當紅黑樹小於此值時又會轉回鏈表
擴充的實際操做不是放在這裏
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //進行樹型化的閾值爲64,若是小於64就不必樹化,會選擇先擴容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //進行擴容 resize(); //找到須要擴容的位置 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { //將鏈表結點轉爲樹狀結點 TreeNode<K,V> p = replacementTreeNode(e, null); //初始化hd,hd爲鏈表的第一個 if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } //tl在剛進入 = hd = p ,而後以後的做用爲遍歷鏈表而後將他們連接起來 tl = p; } while ((e = e.next) != null); //hd爲鏈表的頭節點,先將他賦值給表的固定位置,而後對hd這個鏈表進行樹化 if ((tab[index] = hd) != null) //將這條鏈表樹化 hd.treeify(tab); } }
還未研究紅黑樹,暫且不作解析
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
調用了removeNode這個方法,而後介紹一下這個方法的五個參數。
第一個是hash,天然是計算key的hash
第二個就是key值
第三個是value值
第四個是 是否匹配value,若是值爲true,只刪除值相同的,默認爲false
第五個爲若是爲false,在刪除的時候不移動其餘結點,默認爲true
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爲須要刪除元素的前一個元素 p = e; } while ((e = e.next) != null); } } //在hash表中找到了node,而且node不爲空,而且值相同 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); //node和p結點同樣的狀況,只有在刪除鏈表第一個結點的狀況下 else if (node == p) tab[index] = node.next; //直接將p.next指向須要刪除的結點的下一個 else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
public V get(Object key) { Node<K,V> e; //找到了就直接返回value,沒找到就直接返回null return (e = getNode(hash(key), key)) == null ? null : e.value; }
具體操做是在getNode中
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 && //而且這個位置的數組鏈表不爲null (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); //檢查鏈表中有沒有相等的key,找到就直接返回e do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //找不到就返回null return null; }
這部分沒有什麼好說的,紅黑樹部分後續再講解。
後記: 前五個問題,感謝個人同窗ZR,剩下中有些解釋是我網上找的資料,由於寫的好就直接摘錄了。其他均爲本身的分析和理解,有錯但願指出。