1. 程式人生 > >關於HashMap的一些研究

關於HashMap的一些研究

判斷 重新 復雜 jdk 1.7 equal val style dex

  關於擴容,在resize()方法中,一般是擴容一倍 newCap = oldCap << 1. 擴容的同時,若原table中存在元素,則需要將原table中的元素進行重新計算哈希桶位置.

  在設置初始值的時候,需要將容器大小設置為最接近2次冪的值,例如new HashMap<>(5);則初始容器大小為8.源碼為:

 1 //Returns a power of two size for the given target capacity.
 2 static final int tableSizeFor(int cap) {
 3     int n = cap - 1;
4 n |= n >>> 1; 5 n |= n >>> 2; 6 n |= n >>> 4; 7 n |= n >>> 8; 8 n |= n >>> 16; 9 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 10 }

  為什麽HashMap的容量一定要設置為2次冪呢? 方便put和get. 一般獲取位置使用取余運算%,h % length即可得到哈希值所在的位置,而HashMap中由於容量為2次冪,它使用的是

h & (length - 1),取余運算m % n可以等效於m - m / n * n,位運算比取余運算快了不少.在jdk1.7中,存在方法indexFor用於計算哈希值的位置

1 /**
2  * Returns index for hash code h.
3  */
4 static int indexFor(int h, int length) {
5     // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
6     return h & (length-1);
7 }

而在jdk1.8中,該方法已經不存在了,直接使用在各個方法中,例如 putVal中 p = tab[i = (n - 1) & hash]

  關於哈希沖突,由於 h & (n - 1) 會得到同一個位置的值,就會造成沖突.當沖突小時,HashMap會將該桶轉換成一個鏈表,每個桶的元素為:

1  static class Node<K,V> implements Map.Entry<K,V> {
2         final int hash;
3         final K key;
4         V value;
5         Node<K,V> next;
6         //省略其他代碼
7 }    

可以看出當next沒值時,就沒有哈希沖突,當next有值時,該桶即為一個鏈表結構.當沖突高於某個界限(這個界限為TREEIFY_THRESHOLD = 8)時,該桶將轉變為一顆紅黑樹. 為什麽要轉換成紅黑樹呢? 當存在哈希沖突時,對鏈表的查找復雜度為 O(N),是線性的,當沖突較大時,查找會較慢,此時轉換成紅黑樹,紅黑樹的查找復雜度為O(logN),比鏈表快.源代碼如下:

 1 //是否沖突,不沖突直接插入到該桶
 2 if ((p = tab[i = (n - 1) & hash]) == null)
 3     tab[i] = newNode(hash, key, value, null);
 4 else {
 5   //存在沖突
 6    Node<K,V> e; K k;
 7    if (p.hash == hash &&
 8      ((k = p.key) == key || (key != null && key.equals(k))))
 9        e = p; //key的hash相等以及值也相等,則覆蓋
10    else if (p instanceof TreeNode)
11        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //已經是顆紅黑樹,則往樹裏插入元素
12    else { //鏈表
13        for (int binCount = 0; ; ++binCount) {
14             if ((e = p.next) == null) {
15           //將桶轉變成鏈表
16               p.next = newNode(hash, key, value, null);
17               if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
18            //大於界限,轉換成紅黑樹
19                   treeifyBin(tab, hash);
20                   break;
21              }
22              if (e.hash == hash &&
23                 ((k = e.key) == key || (key != null && key.equals(k))))
24                 break;
25             p = e;
26          }
27      }
28      if (e != null) { // existing mapping for key
29         V oldValue = e.value;
30         if (!onlyIfAbsent || oldValue == null)
31             e.value = value;
32             afterNodeAccess(e);
33             return oldValue;
34      }
35 }

哈希沖突時很常見的

當存在不規範的代碼,即對象hashcode相等當時equals不相等時,該對象當key必存在沖突.

 1         Map<String,String> map = new HashMap<>();
 2         map.put("a","a");
 3         map.put("b","a");
 4         map.put("c","a");
 5         map.put("d","a");
 6         map.put("e","a");
 7         map.put("f","a");
 8         map.put("g","a");
 9         map.put("r","a");
10         map.put("s","a");
11         map.put("t","a");
12         map.put("w","a");
13         map.put("x","a");
14         map.put("y","a");
15         map.put("z","a");    

在上面這段代碼中,跟蹤程序,當執行到14行時,由於存在了13個元素,大於16*0.75,此時擴容,跟蹤HashMap中的table

技術分享

擴容後重新計算元素的哈希桶位置後

技術分享

原先為9個桶13個元素,重新計算後得到13個桶13個元素,可以判斷出此時已經存在哈希沖突. 進一步跟蹤,可以得到沖突的桶:

b -> a.next = r -> a
c -> a.next = s -> a
d -> a.next = t -> a
g -> a.next = w -> a

關於HashMap的一些研究