HashMap是map接口的基礎實現類。這個實現提供了全部可選的Map接口操做。而且容許null鍵和null值。HashMap類和Hashtable類差很少,只是HashMap不是線程徹底的,而且HashMap容許null值和null鍵。這個類不保證map元素的順序,也不保證順序會隨着時間保持不變。java
假如hash函數可以使元素在桶中(buckets)均勻地分散,對於基本的get,put操做HashMap的性能仍是比較穩定的。集合視圖的遍歷(EntrySet之類的操做,也許這些方法返回的結果都是集合類型,因此叫作集合視圖)須要的時間和HashMap的"capacity"(buckets的數據)乘以數量(bucket中的健值對的數量)成正比。所以若是遍歷性能很是重要,那麼就不要把初始的CAPACITY設置的太大(或者LOAD_FACTOR過小)。node
HashMap實例有有兩個屬性影響它的性能:CAPACITY和LOAD_FACTOR。CAPACITY是hash表裏桶的數量,而且初始的CAPACITY僅僅是hash表建立時的容量。LOAD_FACTOR是hash表在自動地增長它的CAPACITY前,容許CAPACITY有多滿的測量方式。當hash表裏的條目的數量超過當前CAPACITY乘以LOAD_FACTOR的數量時,hash表被從新計算hash。(也就是說內部的數據結構被重建)。以便hash表具備大概兩倍於原來桶數量。面試
通常來講,默認的loadfactory(0.75)在時間和空間消耗上提供了一個好的折中。更高的值減少了空間壓力,可是增長了查詢消耗(反映在HashMap中的大部分操做,包括get和put)。爲了減少rehash的操做次數,當設置它的初始capacity時應該考慮未來的map中的條目數量和它的loadfactory。若是初始capacity大於條目最大數量除以loadfactory,就不會有rehash操做發生。算法
若是不少映射(鍵值對)將被存儲在HashMap中。與在須要的時候自動地執行rehash操做來擴大hash表大小相比,建立一個足夠大capacity的hashMap來存儲映射將是更高效的。注意,不少key具備相同的hashCode()值是下降任何hash表性能的方式。數組
注意這個實現不是synchronized(線程安全)的。若是多個線程同時訪問hashMap,而且只要有一個線程修改map結構,它就必須在外面被加上synchronized。(結構的修改是指任何增長或刪除一個或多個的映射,僅僅修改一個健的值不是結構的修改)。這一般經過在自然地包裹map的對象上同步來實現。若是沒有這樣的對象存在。map應該用Collections.synchronizedMap方法包裝一下。爲了防止對map意外的不一樣步的訪問,最好在建立的時候完成這樣的操做。例如安全
Map m = Collections.synchronizedMap(new HashMap(...))數據結構
被這個類的」集合視圖方法」返回的全部遍歷器都是快速失敗的:在這個遍歷器建立以後,用任何方法除了iterator自身的remove方法修改map的結構將會拋出ConcurrentModificationException。所以面對同時的修改,遍歷器快速而乾淨利落地失敗。而不是在不肯定的將來冒着不肯定的危險。app
注意,遍歷器快速失敗的行爲不能被用來保證它看起來的樣子。換句話說,在不一樣步的同時修改前面不能作任何強的擔保。快速失敗的遍歷器儘可能地拋出ConcurrentModificationException。寫的程序依賴這個異常來保證正確性將是錯誤的。iterators的快速失敗行爲應該只被用於檢測錯誤。ide
/**
* 默認的CAPACITY值,也就是16,這個值必須是2的冪
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** * capacity最大值, 當在構造器中傳入一個比這大的參數的時候使用。 * 也就是說,當傳入的值大於這個值,就使用這個值 * 必須是2的冪 */ static final int MAXIMUM_CAPACITY = 1 << 30;
/** * 在構造器中沒有指定的時候被使用的默認的加載因子. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** * 使用樹而不是鏈表的bin(就是以前常說的桶(bucket))中的數量閥值,當在bin中增長數據的時候,大於這個值
* 就會把bin中的數據從鏈表轉換成紅黑樹結構來表示。這個值必須大於2而且應該小
* 小於8。 */
static final int TREEIFY_THRESHOLD = 8;
/** * 在resize操做中把bin中數據變爲列表結構的數量閥值,若是小於這個值,就會 * 從樹結構變爲列表結構。這個值應該小於TREEIFY_THRESHOLD而且最大爲6。 * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6;
/** * 當bin中的結構轉換爲樹的時候,CAPACITY的最小值. * 不然就會resize當bin中數據太多的時候。應該至少4 * TREEIFY_THRESHOLD * 來避免resizing和樹轉換閥值之間的衝突。 * between resizing and treeification thresholds. */ static final int MIN_TREEIFY_CAPACITY = 64;
1 /** 2 * 基本的bin節點, 被用於表示大部分的數據條目。 3 * 4 */ 5 static class Node<K,V> implements Map.Entry<K,V> { 6 final int hash; // 這個記錄的是K的hash值 7 final K key; // map的鍵 8 V value; // map的值 9 Node<K,V> next; // 指向下一個節點 10 11 Node(int hash, K key, V value, Node<K,V> next) { 12 this.hash = hash; 13 this.key = key; 14 this.value = value; 15 this.next = next; 16 } 17 18 public final K getKey() { return key; } 19 public final V getValue() { return value; } 20 public final String toString() { return key + "=" + value; } 21 // 節點的hashCode,key的hashCode和value的hashCode的異或 22 public final int hashCode() { 23 return Objects.hashCode(key) ^ Objects.hashCode(value); 24 } 25 26 public final V setValue(V newValue) { 27 V oldValue = value; 28 value = newValue; 29 return oldValue; 30 } 31 // 重寫的equals,若是節點的key和value都相等,兩個節點才相等。 32 public final boolean equals(Object o) { 33 if (o == this) 34 return true; 35 if (o instanceof Map.Entry) { 36 Map.Entry<?,?> e = (Map.Entry<?,?>)o; 37 if (Objects.equals(key, e.getKey()) && 38 Objects.equals(value, e.getValue())) 39 return true; 40 } 41 return false; 42 } 43 }
/** * 計算key的hashCode值而且把hashCode的高16位和低16位異或。 * 這是一個折中的作法。由於如今大部分狀況下,hash的分佈已經 * 比較分散了,並且若是衝突比較多的時候,咱們會把bin中的數據轉 * 換爲樹結構,來提升搜索速度。 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
爲何採用這樣的算法。或者說爲何要把hashCode的高16位和低16位異或。我一開始也想不明白,看其它的文章也很難找到把這一點解決明白的。函數
因而我就動手作實驗,來驗證,若是不採用異或會怎麼樣。採用異或以後有什麼效果。
固然,不能忘記一點,計算hash值是爲了找這個key對應的table數組之中的下標的。計算下標的算法是tab[i = (n - 1) & hash]。這裏的n是table數組的數量。hash就是hash()方法返回的 值。他們兩個求'與'。在源碼的類說明裏,說了一種狀況,就是幾個連續的Float類型的值在一個小的table中會衝突。我就以幾個連續的Float值爲樣例測試。代碼以下
1 /** 2 * 描述: 3 * 日期:2017年11月13 4 * @author dupang 5 */ 6 public class DupangTest { 7 public static void main(String[] args) { 8 Float f1 = 1f; 9 Float f2 = 2f; 10 Float f3 = 3f; 11 Float f4 = 4f; 12 13 String f1_hashCode = Integer.toBinaryString(f1.hashCode()); 14 String f2_hashCode = Integer.toBinaryString(f2.hashCode()); 15 String f3_hashCode = Integer.toBinaryString(f3.hashCode()); 16 String f4_hashCode = Integer.toBinaryString(f4.hashCode()); 17 18 System.out.println(f1_hashCode); 19 System.out.println(f2_hashCode); 20 System.out.println(f3_hashCode); 21 System.out.println(f4_hashCode); 22 23 int size = 198; 24 int f1_index = f1.hashCode()&(size-1); 25 int f2_index = f2.hashCode()&(size-1); 26 int f3_index = f3.hashCode()&(size-1); 27 int f4_index = f4.hashCode()&(size-1); 28 29 int f1_index_1 = hash(f1)&(size-1); 30 int f2_index_2 = hash(f2)&(size-1); 31 int f3_index_3 = hash(f3)&(size-1); 32 int f4_index_4 = hash(f4)&(size-1); 33 34 System.out.println(f1_index); 35 System.out.println(f2_index); 36 System.out.println(f3_index); 37 System.out.println(f4_index); 38 System.out.println("=========華麗的分割線==========="); 39 System.out.println(f1_index_1); 40 System.out.println(f2_index_2); 41 System.out.println(f3_index_3); 42 System.out.println(f4_index_4); 43 } 44 45 static final int hash(Object key) { 46 int h; 47 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 48 } 49 }
輸出結果以下
111111100000000000000000000000 1000000000000000000000000000000 1000000010000000000000000000000 1000000100000000000000000000000 0 0 0 0 =========華麗的分割線=========== 128 0 64 128
從輸出結果能夠看到,Float類型的1,2,3,4的hashCode都比較大,低位的都是0。若是table的size比較小的時候,和hashCode直接與的話,結果都是0。也就是找到的下標都是同樣的,
因爲在操做過程中就會衝突。
分割線下的結果,就是把hashCode的高16位移到低16位異或,而後計算下標獲得的結果,能夠看到,計算的下標仍是比較分散的,至少比都是0強多了。
這就是計算hash的時候,爲何要把高16位和低16位作異或的緣由了,就是可以讓高16位在計算下標的時候,可以參與進來。
並且在計算hash值的時候,當key等於null的時候,hash值是0。這也是爲何HashMap爲何容許null鍵的緣由。
1 /** 2 * 返回x的Class類對象,若是x實現了接口Comparable<x>。不然就返回Null 3 * Comparable<C>", else null. 4 */ 5 static Class<?> comparableClassFor(Object x) { 6 if (x instanceof Comparable) { 7 Class<?> c; Type[] ts, as; Type t; ParameterizedType p; 8 if ((c = x.getClass()) == String.class) // bypass checks,若是x的類型是String,直接返回x.getClass(),爲何其它的像Integer不直接返回? 9 return c; 10 if ((ts = c.getGenericInterfaces()) != null) {//經過反射獲取c的接口類型。 11 for (int i = 0; i < ts.length; ++i) {//循環,若是 1是參數化類型,2,而且類型是Comparable.class,3,而且參數類型的參數不爲null,4而且參數長度是1,5並 且參數類型是x.getClass();就返回x.getClass(); 12 if (((t = ts[i]) instanceof ParameterizedType) && 13 ((p = (ParameterizedType)t).getRawType() == 14 Comparable.class) && 15 (as = p.getActualTypeArguments()) != null && 16 as.length == 1 && as[0] == c) // type arg is c 17 return c; 18 } 19 } 20 } 21 return null; 22 }
/** * 返回k.compareTo(x)的結果,若是x和k可比較。 * 不然就返回0 */ @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable static int compareComparables(Class<?> kc, Object k, Object x) { return (x == null || x.getClass() != kc ? 0 : ((Comparable)k).compareTo(x)); }
/** * 返回大於等於指定cap值的最小的2的冪.好比cap值是5,計算結果就是8,cap值是16,計算結果仍是16,由於16是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賦值給n。n無符號右移1位而後和n求或,再把結果賦值給n。再把n無符號右移2位而後和n求或,再把結果賦值給n。如此往反。
這有什麼意義呢。這麼作的奧祕何在。光看是不行的。仍是動手吧。以cap值是1073741825爲例來走一遍這個操做。爲何選擇這個數,由於更能看到通過移位後的效果。
看到規律了沒,通過移位以後,他把從高到低的位都變成1了。這還不算。由於都是1的話,換成10進制還不是2的冪。最後還有+1這個操做。二進制加1,1+1=2,逢2進1。後面的一串1都變成0, 最高位進1。至關於左移了一位,低位都變成0。這時候獲得的值纔是2的冪。這時候還能夠聯想一下,一個1從最低位開始左移,左移一位至關於乘以2。左移幾回,至關於乘以幾個2,等到的值固然是2的冪了。
最後強調一點,我舉這個例子是爲了說明右移的效果。若是真是這個值,最後就會大於MAXIMUM_CAPACITY,最後結果就是MAXIMUM_CAPACITY的值,也就是1<<30,2的30次方,固然也是2的冪了,還有爲何用32位表示,由於int在java中就是4個字節,佔32位。還有,若是你的cap是0,n的值是-1;若是本身推結果,別忘記了負數用補碼錶示。
/** * 節點的數組,從這裏能夠看出map的底層實現是數組。這個數組並非 * 在構造方法裏初始化,而是在第一次用到時候初始化它的大小。比較put操做。 * 並且它的數組大小老是2的冪。它的大小也就是前面講的tableSizeFor求得的。 */ transient Node<K,V>[] table;
/** * 持有entrySet()方法的結果. */ transient Set<Map.Entry<K,V>> entrySet;
/** * map中鍵值對的數量. */ transient int size;
/** * 這個HashMap被結構化修改的次數。好比改變鍵值對的數量。或者內部結構的改變(rehash操做)。 * 這個字段被用來遍歷HashMap的集合視圖的快速失敗。 */ transient int modCount;
/** * 觸發resize操做的閥值。當capacity * load factor的值達到這個值的時候,就會執行resize操做。使table的數組擴大。 * */ int threshold;
/** * HashMap的加載因子 */ final float loadFactor;
/** * HashMap的構造方法,能夠指定初始大小和加載因子。通常不多直接用到,由於不多去本身指定加載因子的值。默認的0.75在大部分狀況下都適用 * 當初始值小於0的時候拋異常。 * 當加載由於的值不是正數的時候也拋異常。 * 當指定的初始大小大於MAXIMUM_CAPACITY時。初始大小爲MAXIMUM_CAPACITY。也就是說初始大小不能大於MAXIMUM_CAPACITY。 * 同時也調用tableSizeFor方法計算出下一次resize操做的閥值。這個方法前面詳細講過了。
* 從這裏也能夠看了構造方法裏,並無初始化table的值。它把這個過程日後移了。可能在面試的時候會被問到這一點。
*/ 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的構造方法。能夠指定一個初始大小。加載因子用默認值(0.75) * 這個方法最終調用上面的構造方法。用的最多的就是這個構造方法。*/ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
/** * HashMap構造方法,用默認的初始值(16)和加載因子(0.75)。 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
/** * HashMap的構造方法。參數是一個map。加載因子是默認值。初始大小會根據map參數的大小計算獲得。 * 它會map中的鍵值一個一個地拷貝到HashMap中。當傳入map爲null時會拋空指針。 * 它實際調用的是putMapEntries方法。下面分析一下這個方法*/ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
/** * 這個方法被構造方法和putAll方法調用。當在構造方法中調用的時候evict參數是false。 * 在putAll方法中調用的時候evict參數傳true。這個在HashMap中沒有實際意義。 * 1 計算出map的大小賦值給s,當s的大於0的時候進入下一步 * 2 若是table等於null,就會計算threshold的值,此時仍是沒有初始化table的大小,它把map的size除以加載因子,再加1(爲何要再加1呢?)。
* 獲得的值若是不大於MAXIMUM_CAPACITY,就再判斷是否這個值大於threshold。這時確定是大於的,由於這時threshold還沒賦值,是0;幹嗎還要比較呢,
* 3 根據map的size除以加載由於的值爲參數,求得一個下一次resize操做的閥值賦值給threshold。
* 4 else if的條件是判斷當map的size大於threshold的時候,就會執行resize操做。可是構造方法是否會走到這個邏輯的,只有putAll方法纔有可能走到這個邏輯,咱們一會再看resize邏輯
* 5 最後會遍歷map,以map的key和value執行putVal方法,把map中的鍵值一對一對地put到構造的HashMap中。下面讓咱們先看看putVal的方法。 */ final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 1 if (table == null) { // 2 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) // 3 threshold = tableSizeFor(t); } else if (s > threshold) // 4 resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { // 5 K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
/**
* 1 若是table爲null或者table的長度爲0,就調用resize方法初始化table相關數據。resize具體實現一會再看。從構造方法裏走到這裏的時候,table確定是null的。
* 2 下面就是插入數據的操做。tab[i = (n - 1) & hash],這個比較簡單也比較重要,n是resize後初始化的table數組的大小,它把n-1和k的hash值與操做求得這個key在table中的下標。
* 而後判斷這個下標的值是否等於null,若是等於null,比較happy,說明沒有衝突。就new一個Note節點把key和value賦值進去。而後把這個Note放到table中這個下標的位置。
* 最後modCount加1,由於map的結構變化了。size加1並判斷是否大於threshold,若是大於,就會作resize操做。
* 3 處理key的hash衝突的狀況。
* 3.1 若是老節點的hash和新的hash相等,而且key相等。直接走到 3.6
* 3.2 若是衝突節點的hash值不相等或者key不相等,而後判斷節點類型是不是TreeNode,若是是說明是紅黑樹的結構,就調用putTreeVal方法,
* 3.3 若是衝突節點的ahsh值不相等或者key不相等,而且節點類型不是TreeNode,就走這裏的邏輯,遍歷這個鏈表,先判斷next節點是否爲null,若是是null,說明當前節點是鏈表的最後一個節點。
* 而後就new一個Note節點插入到鏈表的最後。接着判斷遍歷的次數,若是大於等級7就把鏈表結構轉換爲紅黑樹的結構。同時跳出循環
* 3.3.1 若是遍歷的過程當中,下一個節點不爲null,就判斷hash是否相等,而且key是否相等,若是相等,就跳出循環,這時找到的節點的存儲在變量e中。
* 3.4 判斷e是否爲null,不爲null說明存在和要put的key相同的節點。當onlyIfAbsent爲false的時候,也就是key相同時覆蓋舊的值。若是以前key的值爲null也覆蓋舊的值。並返回舊的value值。
* 返回以前調用了一個afterNodeAccess方法,這個方法在HashMap裏是一個空方法。沒有具體意義。走到這裏,是直接返回了,沒有走方法最後幾行的邏輯,由於找到了相同的key節點,並無改變map的結構,size大小也沒變。因此就直 * 接返回了。
* 4 最後 modCount加1,所明map的結構發生改變了。而且判斷size加1後是否大於閥值,若是大於就觸發發resize的條件,進行rezize。一樣調用了afterNodeAccess方法,最後返回null,也說明put的是一個新值,沒有key相同的節點
下面讓咱們看看resize方法都作了什麼
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2 boolean evict) { 3 Node<K,V>[] tab; Node<K,V> p; int n, i; 4 if ((tab = table) == null || (n = tab.length) == 0) // 1 5 n = (tab = resize()).length; 6 if ((p = tab[i = (n - 1) & hash]) == null) // 2 7 tab[i] = newNode(hash, key, value, null); 8 else { // 3 9 Node<K,V> e; K k; 10 if (p.hash == hash && 11 ((k = p.key) == key || (key != null && key.equals(k)))) // 3.1 12 e = p; 13 else if (p instanceof TreeNode) // 3.2 14 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 15 else { 16 for (int binCount = 0; ; ++binCount) { // 3.3 17 if ((e = p.next) == null) { 18 p.next = newNode(hash, key, value, null); 19 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 20 treeifyBin(tab, hash); 21 break; 22 } 23 if (e.hash == hash && 24 ((k = e.key) == key || (key != null && key.equals(k)))) // 3.3.1 25 break; 26 p = e; 27 } 28 } 29 if (e != null) { // 3.4 30 V oldValue = e.value; 31 if (!onlyIfAbsent || oldValue == null) 32 e.value = value; 33 afterNodeAccess(e); 34 return oldValue; 35 } 36 } 37 ++modCount; // 4 38 if (++size > threshold) 39 resize(); 40 afterNodeInsertion(evict); 41 return null; 42 }
/** * 最經常使用的put方法,這個方法一看上去很簡單,其實具體實現都在putVal方法裏。*/ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
/** * 初始化一個table或使原來的table大小翻倍. 若是table爲null,就以threshold的值做爲初始大小來分配table。 * 1 先初始化幾個變量值,把當前table變量賦值給oldTab,若是oldTab是null,說明table尚未初始化,oldCap就爲0。不然就是table的長度。把老的threshold賦值給oldThr中。 * 2 若是oldCap大於0,因此以前table已經被初如化過,也就是map裏以前有值。
2.1 這時候看oldCap是否大於MAXMUM_CAPACITY,若是大於等於,就乾脆把threshold定爲Integer最大值。也不必再乘以2了,由於MAXMUM_CAPACITY已 經是2的30次方了。再乘以2就越界了。 * 2.2 若是oldCap大於0而且小於MAXMUM_CAPACITY。就進入這個邏輯塊,若是oldCap左移一位(乘以2)後仍是小於MAXMUM_CAPACITY而且oldCap大於默認的CAPACITY(16),並把oldThr左移一位(乘以2),存到newThr變量中。
3 若是oldCap小於等於0說明。以前沒有初始化table,這時候判斷oldThr,若是大於0,就把閥值看成table大小賦值給newCap。 * 4 不然就把新的table大小設置爲默認值16,並根據默認值計算出resize的閥值。 * 5 判斷新的閾值是不是0,若是走到3的條件裏,就會是這種狀況,這時會根據新的cap和默認的加載因子(0.75)。若是新的cap和閾值都小於MAXMUM_CAPACITY。就把計算出的閾值賦值給newThr e
6 而後根據newCap大小,new一個Node數組,並把這個數組賦值給table。
7 若是老的table不爲空,就要把老的table一個一個的copy到新的table裏。
7.1 遍歷老table中的元素
7.1.1 若是table數組中的這個下標不爲空,就準備copy到新的table裏
7.1.2 若是table數組中的這個下標不爲空,而且next結點爲空,說明只有一個元素。沒有連接結構。就根據hash和新的數組大小求一個下標,放到新table的這個下標裏。
7.1.3 若是這個節點是樹結點。就進行樹操做。
7.1.4 若是數據的這個下標有空,而且這個節點還有next節點。就把連接結構裏的節點也一併cp到新的table裏。這裏有一點不一樣,就是若是這個節點和老的數組大小求與結果是0,就把這個節點仍是放到新的table數組的
的相同下標的位置,不然就移動oldCap個下標放置。有點不太明白。直接用if的邏輯不就好了。把e賦值過去,e.next這一大串不也跟着過去了麼。
8 最後返回擴容後的table * @return the table */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; // 1 int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 2 if (oldCap >= MAXIMUM_CAPACITY) { // 2.1 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 2.2 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 3 initial capacity was placed in threshold newCap = oldThr; else { // 4// zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 5 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 6 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 7 for (int j = 0; j < oldCap; ++j) { // 7.1 Node<K,V> e; if ((e = oldTab[j]) != null) { // 7.1.1 若是table數組中的這個下標不爲空,就準備copy到新的table裏 oldTab[j] = null; if (e.next == null) // 7.1.2 若是table數組中的這個下標不爲空,而且next結點爲空,說明只有一個元素。沒有連接結構。就根據hash和新的數組大小求一個下標,放到新table的這個下標裏。 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 7.1.3 若是這個節點是樹結點。就進行樹操做。 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order // 7.1.4 若是數據的這個下標有空,而且這個節點還有next節點。就把連接結構裏的節點也一併cp到新的table裏。這裏有一點不一樣,就是若是這個節點和老的數組大小求與結果是0,就把這個節點仍是放到新的table數組的
的相同下標的位置,不然就移動oldCap個下標放置。有點不太明白。直接用if的邏輯不就好了。把e賦值過去,e.next這一大串不也跟着過去了麼。
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; }
下面咱們來看一下,當鏈表中的元素大於8個時,怎麼怎麼把單鏈錶轉成樹的。
/** * 1 而table等於null的或table的長度小於MIN_TREEIFY_CAPACITY(64)的,並不會去轉成紅黑樹,而是進行resize。因此鏈表結構轉成紅黑樹,須要知足兩個條件。1 鏈表的元素大於8個。2 table的數組長度大於64. * 2 根據hash計算得出下標,獲取這個下標中的值。而後遍歷。把以這個下標元素爲頭的單連接表中的每個元素。都轉成TreeNode。TreeeNode其實繼承於LinkedHashMap。因此它也是一個兩向鏈表。在轉成紅黑樹以前。把單鏈表中的節點轉成TreeNode的同時。也把單鏈錶轉成了
雙向鏈表。而後再調用TreeNode的方法hd.treeify(tab)。去把雙向鏈表轉成紅黑樹。 */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 1 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { // 2 TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
/** * 這裏就是轉成紅黑樹的核心代碼。若是不知道什麼是紅黑樹的,就不要繼續看下去了。先是先去看看什麼是紅黑樹再說吧。
* 這裏大部分代碼就是再鏈表的節點遍歷。作紅黑樹的插入。由於紅黑樹也是二叉搜索樹,因此插入的時候也是小的在左邊。大的右邊。比較大小的時候,是用的hash值比較的。插入完後。由於可能會違反紅黑樹的性質。因此就須要調用balanceInsertion這個方法
* 作一些從新着色和左旋和右旋這樣的操做。最後使節點插入後,依然是一棵紅黑樹。因此看懂了紅黑樹。看這部分代碼就很容易多了。 * @return root of tree */ final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); }
在resize的文中。當節點是TreeNode時。會調用TreeNode的split方法。下面咱們看看這個方法。
/** * 這個方法和以前的鏈表結構的移動有點相似。就是遍歷這個Tree。把節點的hash值和老的數組大小求與。若是是0.就把這些數據,放到新的table和老的table相同下標的位置。不然就偏移ol dCap個位置放置。低位和高位的結構分別放在loHead和hiHead裏。當loHead不爲空的時候。還會判斷lc的數值。它記錄的是loHead結構的節點個數。若是小於等於6個。就會把樹結構轉爲
鏈表結構。調用的untreeify方法。這個方法比較簡單。就是遍歷樹。把TreeNode節點轉爲Node節點。若是lohead和hiHead都不爲空。說明原來的樹結構改變了。可能就違背了紅黑樹的性 質。就會重度調一下treeify方法。
* or untreeifies if now too small. Called only from resize; * see above discussion about split bits and indices. * * @param map the map * @param tab the table for recording bin heads * @param index the index of the table being split * @param bit the bit of hash to split on */ final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } if (loHead != null) { if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { tab[index] = loHead; if (hiHead != null) // (else is already treeified) loHead.treeify(tab); } } if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } }
put差很少說完了,下面看一下get方法
/** * 返回指定的key對應的值。或者若是指定key對應的值就返回null, * 返回null並不必定意爲沒有指定key對應的值。也可能它的值就是null。這時 * 能夠用containsKey的方法來區分是否包含key。 這個方法主體是,調用getNode方法獲取節點,若是這個節點等於null就返回null。
不然就返回節點的值。主要邏輯都在getNode方法裏。下面看一下這個方法。 * * @see #put(Object, Object) */ public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
/** * 1 首先先判斷這個table不等於null。table數組長度大於0。根據hash求得的下標對應的數組元素不等於null。纔會進入裏面的邏輯,不然就會直接返回null。 * 2 而後首先比較找到的元素的hash和傳入的hash是否相等,而且key相等。若是都相等。因此這個正好就是要找的節點。直接返回。
3 不然,若是找到的節點還有next節點。就會遍歷以找到的節點爲頭的鏈表。一個一個地比較hash和key是否相等。若是相等就返回找到的節點。
4 若是找到的節點是TreeNode類型的節點,說明就是一個紅黑樹。就會調用getTreeNode方法進行樹結構的查找。
* @param hash hash for key * @param key the key * @return the node, or null if none */ 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) { // 1 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; }
/** * 這個是先找到樹的根節點,若是parent不爲空,就說明它不是根節點,就經過root方法返回這個節點所在樹的根節點,而後從根節點調用find方法查詢。 */ final TreeNode<K,V> getTreeNode(int h, Object k) { return ((parent != null) ? root() : this).find(h, k, null); }
/** * 前兩個if else就跟普通的二叉查找樹的邏輯差很少了。這裏是以hash來比較大小的。
若是p的hash大於傳入的hash值,就去從p的左孩子繼續找。若是p的hash小於傳入的hash。就去從p的右孩子繼續找。
若是相同,就比較key是否相等,若是相等。說明找到了,就直接返回。不然就進入其它的elseif
* The kc argument caches comparableClassFor(key) upon first use * comparing keys. */ final TreeNode<K,V> find(int h, Object k, Class<?> kc) { TreeNode<K,V> p = this; do { int ph, dir; K pk; TreeNode<K,V> pl = p.left, pr = p.right, q; if ((ph = p.hash) > h) p = pl; else if (ph < h) p = pr; else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; else if (pl == null) // 若是左孩子爲空。就把右孩子賦值給p繼續找。 p = pr; else if (pr == null) // // 若是右孩子爲空。就把左孩子賦值給p繼續找。 p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) //這時hash是不會相等的。而且左右孩子都不爲空。就去看key是否有可比性,而且 根據key的比較結果還判斷,是從左孩子繼續找,仍是從右孩子繼續找。 p = (dir < 0) ? pl : pr; else if ((q = pr.find(h, k, kc)) != null) // 若是根據key也沒有比較結果的話,那就乾脆從右孩子繼續找吧。 return q; else p = pl; } while (p != null); return null; }
下面看一下clear方法
/** * 它只是遍歷table數組,而後把每個數組元素賦值給null */ public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } }
remove方法
/** * 若是指定的key存在,就刪除指定key對應的鍵值對。 * 返回值是key對應的值,若是沒有對應key的鍵值對。就返回null。返回null
* 並不意爲着沒有這個key的鍵值對,也多是這個key對應的值就是null。 * 刪除方法主要邏輯都在removeNode裏。*/ public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
removeNode()
/** * 這個方法,有一半邏輯都是在查找要刪除的節點,這些內容在get方法裏已經說過,再也不細說。 * 主要是在若是找到的node不爲null。再進入具體的刪除邏輯,這時候還會判斷,matchValue,若是matchValue爲true,當value值和找到的節點的值相等纔會刪除。
* 若是找到的節點類型是TreeNode會調用removeTreeNode來進行刪除,這一部分,主要仍是紅黑樹的刪除,再也不細說。不瞭解紅黑樹的,最好仍是先理解紅黑樹,否則不容易看懂。
*
* @param key的hash值 * @param key * @param key的值,在matchValue爲ture的時候,會比較value的值,當key和value都相等時才刪除。 * @param 在matchValue爲ture的時候,會比較value的值,當key和value都相等時才刪除。 * @param 當movable爲false時,當刪除一個節點時,不會移動其它節點。 * @return the node, or null if none */ 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); } } 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; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }