Java8中的HashMap分析


本篇文章是網上多篇文章的精華的總結,結合自己看源代碼的一些感悟,其中線程安全性和性能測試部分並未做實踐測試,直接是“拿來”網上的博客的。

哈希表概述

哈希表本質上一個數組,數組中每一個元素稱為一個箱子(Bin),箱子中存放的是鍵值對Entry<K,V>鏈表,因而也稱之為鏈表散列。

我們可以用圖來形象地說明這個結構:

哈希表是如何工作的?

存儲

Step1:根據哈希函數來計算HashCode值h,其中鍵值對Entry<K,V>的K來計算時需要的參數。

Step2:根據HashCode,來計算存放在哈希表(長度為n)中的位置(箱子的位置),一種計算方法是取余:h%n。

Step3:如果該箱子中已經存在鍵值對數據,則使用開放尋址法或拉鏈法解決沖突。

獲取

Step1:根據key值計算HashCode的值h。

Step2:假設箱子的個數為 n,那么這個鍵值對應該放在第 (h % n) 個箱子中。

Step3:如果這個箱子里有多個鍵值對,同時假設箱子里的多個值是采用鏈表的方式存儲,則需要遍歷這個鏈表,復雜度為O(n)。

擴容

哈希表還有 一個重要的屬性:負載因子,它是衡量哈希表的空/滿程度,一定程度上也能體現查詢的效率。其計算公式為:

負載因子 = 總鍵值對數 / 箱子數量

負載因子越大,意味着哈希表越滿,越容易導致沖突(更大的概念找到同一個箱子上),因而查詢效率也就更低。因而,一般來說,當負載因子大於某個常數(可能是1,也可能是其他值,Java8的HashMap的負載因子為0.75)時,哈希表就會自動擴容。

哈希表在擴容的時候,一般都會選擇擴大2的倍數,同時將原來的哈希表的數據遷移到新的哈希表中,這樣即使key的哈希值不變,對箱子的取余結果(假設我們用這種方法來計算HashCode)也會不同,因此所有的箱子和元素的存放位置都有可能發生變化,這個過程也稱為重哈希(rehash)。

哈表的擴容並不能有效解決負載因子過大的問題,因為在前面的取HashCode的方法中,假設所有key的HashCode值都一樣,那么即使擴容以后他們在哈希表中的位置也不會變,實際存放在箱子中的鏈表長度也不變,因此也就不能提高哈希表的查詢速度。

因而,哈希表存在以下兩個問題:

 1、在擴容的時候,重哈希的成本比較大

 2、如果Hash函數設計地不合理(如上面舉例說明的取余),會導致哈希表中極端情況下變成線性表,性能極低。

我們下面來看看Java8中是如何處理這兩個問題的。

以上這部分內容多參考自:深入理解哈希表 ,圖片來自於HashMap的圖示

 

Java8中的HashMap

在說明這個問題之前,我們來看下HashMap在Java8中在類圖關系,如下所示:

Java8中通過如下幾種方式來解決上面的兩個問題:

一、讓元素分布地更合理

      (下面這部分不知道是哪位大神寫的,原文照抄吧)

      學過概率論的讀者也許知道,理想狀態下哈希表的每個箱子中,元素的數量遵守泊松分布:

      

      當負載因子為 0.75 時,上述公式中 λ 約等於 0.5,因此箱子中元素個數和概率的關系如下:

      

數量 概率
0 0.60653066
1 0.30326533
2 0.07581633
3 0.01263606
4 0.00157952
5 0.00015795
6 0.00001316
7 0.00000094
8 0.00000006

      這就是為什么我們將0.75設為負載因子,同時針對箱子中鏈表長度超過8以后要做另外的優化(一來是優化的概念較小,二來是優化過后的效率提升明顯)。所以,一般情況下負載因子不建議修改;同時如果在數量為8的鏈表的概率較大,則幾乎可以認為是哈希函數設計有問題導致的。

二、通過紅黑樹讓查詢更有效率(O(n)—>O(Log(n)))

       第一點已經說明,當箱子中的鏈表元素超過8個時,會將這個鏈表轉為紅黑樹,紅黑樹的查找效率為O(log(n))。紅黑樹的示圖如下:

      

 

三、讓擴容時重哈希(rehash)的成本變得更小

       在Java7中,重哈希是要重新計算Hash值的,而在Java8中,通過高位運算的巧妙設計,避免了這種計算。下面我們舉例說明:

      我們要在初始大小為2的HashMap中存儲3、5、7這3個值,Hash函數為取余法。

      Step1:在開始的時候,3、5、7經過Hash過后 3%2=1、5%2=1、7%2=1,因而3、5、7存儲在同一個箱子的鏈表中(地址為1)。

      Step2:現在擴容了,擴容后的大小為2*2=4,現在經過Hash后3%4=3、5%4=1、7%4=3,因而3與7一起放在箱子的鏈表中(地址為3),5單獨存放在一個箱子里(地址為1)。

      整個過程如下圖所示:

      

      我們注意到,在擴容后3和7的位置變化了,由1—>3(=1+2)

      再進行擴容,由4容為8,那么經過Hash后,3%8=3、5%8=5、7%8=7,分別存放於3、5(=1+4)、7(=3+4)這幾個位置中。

      我們發現,擴容后的元素要么在原位置,要么在原位置再移動2次冪的位置,整個過程只需要使用一個位運算符<<就可以了(在源碼的resize方法中可以找到)。

      我們用計算機的地址來展示這個過程:

      

 

      n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:

 

      因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:

     

      這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。

      以上這部分中的圖示和位移講解的內容參考自:深入分析hashmap

另外:

四:我們可以通過適當地初始化大小來控制擴容的次數:既然擴容是不可避免的,我們就盡可能少地讓它發生,要實際編程的時候,應該根據業務合理地設置初始大小的值。

此外,Java8中HashMap還提供了另外一些參數來控制HashMap的性能,如下所示:

 /** * 默認的初始化大小(必須為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; /** * HashMap由鏈表轉為紅黑樹存儲的閥值 * 1.8提供的新特性 */
    static final int TREEIFY_THRESHOLD = 8; /** * HashMap由紅黑樹轉為鏈表存儲的閥值 */
    static final int UNTREEIFY_THRESHOLD = 6; /** * HashMap的箱子中的鏈表轉為紅黑樹之前還有一個判斷: * 只在所有箱子(鍵值對)的數量大於64才會發生轉換 * 這樣是為了避免在哈希表建立初期,多個鍵值對恰好被放入了同一個鏈表而導致不必要的轉化 */
    static final int MIN_TREEIFY_CAPACITY = 64;

 

 

源碼中的關鍵方法

 方法一、hash方法

1 static final int hash(Object key) { 2         int h; 3         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//這里其實就要求大家來重寫HashCode方法 4     }

 

 方法二、putVal方法

下面是putVal方法的執行過程圖示:

 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      // 步驟①:tab為空則創建
 5      if ((tab = table) == null || (n = tab.length) == 0)  6          n = (tab = resize()).length;  7      // 步驟②:計算index,並對null做處理
 8      if ((p = tab[i = (n - 1) & hash]) == null)  9          tab[i] = newNode(hash, key, value, null); 10      else { 11          Node<K,V> e; K k; 12          // 步驟③:節點key存在,直接覆蓋value
13          if (p.hash == hash &&
14              ((k = p.key) == key || (key != null && key.equals(k)))) 15              e = p; 16          // 步驟④:判斷該鏈為紅黑樹
17          else if (p instanceof TreeNode) 18              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 19         // 步驟⑤:該鏈為鏈表
20          else { 21              for (int binCount = 0; ; ++binCount) { 22                  if ((e = p.next) == null) { 23                      p.next = newNode(hash, key,value,null); 24                         //鏈表長度大於8轉換為紅黑樹進行處理
25                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 
26  treeifyBin(tab, hash); 27                      break; 28  } 29                     // key已經存在直接覆蓋value
30                  if (e.hash == hash &&
31                      ((k = e.key) == key || (key != null && key.equals(k)))) 32                             break; 33                  p = e; 34  } 35  } 36         
37          if (e != null) { // existing mapping for key
38              V oldValue = e.value; 39              if (!onlyIfAbsent || oldValue == null) 40                  e.value = value; 41  afterNodeAccess(e); 42              return oldValue; 43  } 44  } 45      ++modCount; 46      // 步驟⑥:超過最大容量 就擴容
47      if (++size > threshold) 48  resize(); 49  afterNodeInsertion(evict); 50      return null; 51  }
HashMap的putVal方法

 

      這個 getNode() 方法就是根據哈希表元素個數與哈希值求模(使用的公式是 (n - 1) &hash)得到 key 所在的桶的頭結點,如果頭節點恰好是紅黑樹節點,就調用紅黑樹節點的 getTreeNode() 方法,否則就遍歷鏈表節點。

 

 方法三、節點查找方法getNode 

 1 final Node<K,V> getNode(int hash, Object key) {  2     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;  3     if ((tab = table) != null && (n = tab.length) > 0 &&
 4         (first = tab[(n - 1) & hash]) != null) {  5        if (first.hash == hash && // always check first node
 6             ((k = first.key) == key || (key != null && key.equals(k))))  7             return first;  8         if ((e = first.next) != null) {  9             if (first instanceof TreeNode) 10                 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 11             do { 12                if (e.hash == hash &&
13                     ((k = e.key) == key || (key != null && key.equals(k)))) 14                     return e; 15             } while ((e = e.next) != null); 16  } 17  } 18     return null; 19 }
HashMap的查找節點方法

   

方法四、紅黑樹生成方法

 1 //將桶內所有的 鏈表節點 替換成 紅黑樹節點
 2 
 3 final void treeifyBin(Node<K,V>[] tab, int hash) {  4 
 5     int n, index; Node<K,V> e;  6 
 7     //如果當前哈希表為空,或者哈希表中元素的個數小於 進行樹形化的閾值(默認為 64),就去新建/擴容
 8 
 9    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 10 
11  resize(); 12 
13     else if ((e = tab[index = (n - 1) & hash]) != null) { 14 
15         //如果哈希表中的元素個數超過了 樹形化閾值,進行樹形化 16 
17         // e 是哈希表中指定位置桶里的鏈表節點,從第一個開始
18 
19         TreeNode<K,V> hd = null, tl = null; //紅黑樹的頭、尾節點
20 
21         do { 22 
23             //新建一個樹形節點,內容和當前鏈表節點 e 一致
24 
25             TreeNode<K,V> p = replacementTreeNode(e, null); 26 
27             if (tl == null) //確定樹頭節點
28 
29                 hd = p; 30 
31            else { 32 
33                p.prev = tl; 34 
35                 tl.next = p; 36 
37  } 38 
39             tl = p; 40 
41         } while ((e = e.next) != null); 42 
43         //讓桶的第一個元素指向新建的紅黑樹頭結點,以后這個桶里的元素就是紅黑樹而不是鏈表了
44 
45         if ((tab[index] = hd) != null) 46 
47  hd.treeify(tab); 48 
49  } 50 
51  } 52 
53     TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { 54 
55     return new TreeNode<>(p.hash, p.key, p.value, next); 56 
57  }
紅黑樹生成方法

  

方法五、紅黑樹節點查找方法

 1 final TreeNode<K,V> find(int h, Object k, Class<?> kc) {  2             TreeNode<K,V> p = this;  3             do {  4                 int ph, dir; K pk;  5                 TreeNode<K,V> pl = p.left, pr = p.right, q;  6                 if ((ph = p.hash) > h)  7                     p = pl;  8                 else if (ph < h)  9                     p = pr; 10                 else if ((pk = p.key) == k || (k != null && k.equals(pk))) 11                     return p; 12                 else if (pl == null) 13                     p = pr; 14                 else if (pr == null) 15                     p = pl; 16                 else if ((kc != null ||
17                           (kc = comparableClassFor(k)) != null) &&
18                          (dir = compareComparables(kc, k, pk)) != 0) 19                     p = (dir < 0) ? pl : pr; 20                 else if ((q = pr.find(h, k, kc)) != null) 21                     return q; 22                 else
23                     p = pl; 24             } while (p != null); 25             return null; 26         }
紅黑樹查找方法

  

方法六、擴容方法

 1 final Node<K,V>[] resize() {  2     Node<K,V>[] oldTab = table;  3     int oldCap = (oldTab == null) ? 0 : oldTab.length;  4     int oldThr = threshold;  5     int newCap, newThr = 0;  6     if (oldCap > 0) {  7         // 超過最大值就不再擴充了,就只好隨你碰撞去吧
 8         if (oldCap >= MAXIMUM_CAPACITY) {  9             threshold = Integer.MAX_VALUE; 10             return oldTab; 11  } 12         // 沒超過最大值,就擴充為原來的2倍
13         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
14                  oldCap >= DEFAULT_INITIAL_CAPACITY) 15             newThr = oldThr << 1; // double threshold
16  } 17     else if (oldThr > 0) // initial capacity was placed in threshold
18         newCap = oldThr; 19     else {               // zero initial threshold signifies using defaults
20         newCap = DEFAULT_INITIAL_CAPACITY; 21         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 22  } 23     // 計算新的resize上限
24     if (newThr == 0) { 25 
26         float ft = (float)newCap * loadFactor; 27         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
28                   (int)ft : Integer.MAX_VALUE); 29  } 30     threshold = newThr; 31     @SuppressWarnings({"rawtypes","unchecked"}) 32         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 33     table = newTab; 34     if (oldTab != null) { 35         // 把每個bucket都移動到新的buckets中
36         for (int j = 0; j < oldCap; ++j) { 37             Node<K,V> e; 38             if ((e = oldTab[j]) != null) { 39                 oldTab[j] = null; 40                 if (e.next == null) 41                     newTab[e.hash & (newCap - 1)] = e; 42                 else if (e instanceof TreeNode) 43                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 44                 else { // preserve order
45                     Node<K,V> loHead = null, loTail = null; 46                     Node<K,V> hiHead = null, hiTail = null; 47                     Node<K,V> next; 48                     do { 49                         next = e.next; 50                         // 原索引
51                         if ((e.hash & oldCap) == 0) { 52                             if (loTail == null) 53                                 loHead = e; 54                             else
55                                 loTail.next = e; 56                             loTail = e; 57  } 58                         // 原索引+oldCap
59                         else { 60                             if (hiTail == null) 61                                 hiHead = e; 62                             else
63                                 hiTail.next = e; 64                             hiTail = e; 65  } 66                     } while ((e = next) != null); 67                     // 原索引放到bucket里
68                     if (loTail != null) { 69                         loTail.next = null; 70                         newTab[j] = loHead; 71  } 72                     // 原索引+oldCap放到bucket里
73                     if (hiTail != null) { 74                         hiTail.next = null; 75                         newTab[j + oldCap] = hiHead; 76  } 77  } 78  } 79  } 80  } 81     return newTab; 82 }
HashMap擴容方法

  

方法七、擴容后的元素轉移方法

 1  void transfer(Entry[] newTable) {  2      Entry[] src = table;                   //src引用了舊的Entry數組
 3      int newCapacity = newTable.length;  4      for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組
 5          Entry<K,V> e = src[j];             //取得舊Entry數組的每個元素
 6          if (e != null) {  7              src[j] = null;//釋放舊Entry數組的對象引用(for循環后,舊的Entry數組不再引用任何對象)
 8              do {  9                  Entry<K,V> next = e.next; 10                  int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在數組中的位置
11                  e.next = newTable[i]; //標記[1]
12                  newTable[i] = e;      //將元素放在數組上
13                  e = next;             //訪問下一個Entry鏈上的元素
14              } while (e != null); 15  } 16  } 17 17 }
擴容后的元素轉移

 

線程安全性

在多線程使用場景中,應該盡量避免使用線程不安全的HashMap,而使用線程安全的ConcurrentHashMap。那么為什么說HashMap是線程不安全的,下面舉例子說明在並發的多線程使用場景中使用HashMap可能造成死循環。代碼例子如下(便於理解,仍然使用JDK1.7的環境): 

 1 public class HashMapInfiniteLoop {  2  
 3     private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f);  4     public static void main(String[] args) {  5         map.put(5, "C");  6  
 7         new Thread("Thread1") {  8             public void run() {  9                 map.put(7, "B"); 10  System.out.println(map); 11  }; 12  }.start(); 13         new Thread("Thread2") { 14             public void run() { 15                 map.put(3, "A);  
16  System.out.println(map); 17  }; 18  }.start(); 19  } 20 }

 

其中,map初始化為一個長度為2的數組,loadFactor=0.75,threshold=2*0.75=1,也就是說當put第二個key的時候,map就需要進行resize。

通過設置斷點讓線程1和線程2同時debug到transfer方法(3.3小節代碼塊)的首行。注意此時兩個線程已經成功添加數據。放開thread1的斷點至transfer方法的“Entry next = e.next;” 這一行;然后放開線程2的的斷點,讓線程2進行resize。結果如下圖:

注意,Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。

線程一被調度回來執行,先是執行 newTalbe[i] = e, 然后是e = next,導致了e指向了key(7),而下一次循環的next = e.next導致了next指向了key(3)。

 

 

e.next = newTable[i] 導致 key(3).next 指向了 key(7)。注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。

於是,當我們用線程一調用map.get(11)時,悲劇就出現了——Infinite Loop。

 

性能表現:JDK1.8 vs JDK1.7

HashMap中,如果key經過hash算法得出的數組索引位置全部不相同,即Hash算法非常好,那樣的話,getKey方法的時間復雜度就是O(1),如果Hash算法技術的結果碰撞非常多,假如Hash算極其差,所有的Hash算法結果得出的索引位置一樣,那樣所有的鍵值對都集中到一個桶中,或者在一個鏈表中,或者在一個紅黑樹中,時間復雜度分別為O(n)和O(lgn)。 鑒於JDK1.8做了多方面的優化,總體性能優於JDK1.7,下面我們從兩個方面用例子證明這一點。

Hash較均勻的情況

為了便於測試,我們先寫一個類Key,如下:

 1 class Key implements Comparable<Key> {  2  
 3     private final int value;  4  
 5     Key(int value) {  6         this.value = value;  7  }  8  
 9  @Override 10     public int compareTo(Key o) { 11         return Integer.compare(this.value, o.value); 12  } 13  
14  @Override 15     public boolean equals(Object o) { 16         if (this == o) return true; 17         if (o == null || getClass() != o.getClass()) 18             return false; 19         Key key = (Key) o; 20         return value == key.value; 21  } 22  
23  @Override 24     public int hashCode() { 25         return value; 26  } 27 }

 

這個類復寫了equals方法,並且提供了相當好的hashCode函數,任何一個值的hashCode都不會相同,因為直接使用value當做hashcode。為了避免頻繁的GC,我將不變的Key實例緩存了起來,而不是一遍一遍的創建它們。代碼如下:

 1 public class Keys {  2  
 3     public static final int MAX_KEY = 10_000_000;  4     private static final Key[] KEYS_CACHE = new Key[MAX_KEY];  5  
 6     static {  7         for (int i = 0; i < MAX_KEY; ++i) {  8             KEYS_CACHE[i] = new Key(i);  9  } 10  } 11  
12     public static Key of(int value) { 13         return KEYS_CACHE[value]; 14  } 15 }

現在開始我們的試驗,測試需要做的僅僅是,創建不同size的HashMap(1、10、100、……10000000),屏蔽了擴容的情況,代碼如下:

 1 static void test(int mapSize) {  2  
 3        HashMap<Key, Integer> map = new HashMap<Key,Integer>(mapSize);  4        for (int i = 0; i < mapSize; ++i) {  5  map.put(Keys.of(i), i);  6  }  7  
 8        long beginTime = System.nanoTime(); //獲取納秒
 9        for (int i = 0; i < mapSize; i++) { 10  map.get(Keys.of(i)); 11  } 12        long endTime = System.nanoTime(); 13        System.out.println(endTime - beginTime); 14  } 15  
16    public static void main(String[] args) { 17        for(int i=10;i<= 1000 0000;i*= 10){ 18  test(i); 19  } 20    }

在測試中會查找不同的值,然后度量花費的時間,為了計算getKey的平均時間,我們遍歷所有的get方法,計算總的時間,除以key的數量,計算一個平均值,主要用來比較,絕對值可能會受很多環境因素的影響。結果如下:

通過觀測測試結果可知,JDK1.8的性能要高於JDK1.7 15%以上,在某些size的區域上,甚至高於100%。由於Hash算法較均勻,JDK1.8引入的紅黑樹效果不明顯,下面我們看看Hash不均勻的的情況。

Hash極不均勻的情況

假設我們又一個非常差的Key,它們所有的實例都返回相同的hashCode值。這是使用HashMap最壞的情況。代碼修改如下:

1 class Key implements Comparable<Key> { 2  
3     //...
4  
5  @Override 6     public int hashCode() { 7         return 1; 8  } 9 }

  仍然執行main方法,得出的結果如下表所示:

從表中結果中可知,隨着size的變大,JDK1.7的花費時間是增長的趨勢,而JDK1.8是明顯的降低趨勢,並且呈現對數增長穩定。當一個鏈表太長的時候,HashMap會動態的將它替換成一個紅黑樹,這話的話會將時間復雜度從O(n)降為O(logn)。hash算法均勻和不均勻所花費的時間明顯也不相同,這兩種情況的相對比較,可以說明一個好的hash算法的重要性。

 

小結

1.有兩個字典,分別存有 100 條數據和 10000 條數據,如果用一個不存在的 key 去查找數據,在哪個字典中速度更快?

完整的答案是:在 Redis 中,得益於自動擴容和默認哈希函數,兩者查找速度一樣快。在 Java 和 Objective-C 中,如果哈希函數不合理,返回值過於集中,會導致大字典更慢。Java 由於存在鏈表和紅黑樹互換機制,搜索時間呈對數級增長,而非線性增長。在理想的哈希函數下,無論字典多大,搜索速度都是一樣快。

2. 擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。

3. 負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。

4. HashMap是線程不安全的,不要在並發的環境中同時操作HashMap,建議使用ConcurrentHashMap。

5. JDK1.8引入紅黑樹大程度優化了HashMap的性能。

 

參考文檔

https://tech.meituan.com/java-hashmap.html

https://www.cnblogs.com/gotodsp/p/6534699.html

https://www.cnblogs.com/shengkejava/p/6771469.html

http://www.importnew.com/14417.html 這篇講性能的文章值得一看

http://alex09.iteye.com/blog/539545 這是講Java7中的HashMap的

https://www.cnblogs.com/chinajava/p/5808416.html 這里面講了Java的HashMap和Redis的HashMap對比

http://blog.csdn.net/Richard_Jason/article/details/53887222

http://www.importnew.com/7099.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM