1. 程式人生 > >Java HashMap的擴容

Java HashMap的擴容

ins 變量 ext pla pop hold toc 函數 理解

最近博主參加面試,發現自己對於Java的HashMap的擴容過程理解不足,故最近在此進行總結。

首先說明博主德Java為1.8版本

HashMap中的變量

首先要了解HashMap的擴容過程,我們就得了解一些HashMap中的變量:

  • Node<K,V>:鏈表節點,包含了key、value、hash、next指針四個元素
  • table:Node<K,V>類型的數組,裏面的元素是鏈表,用於存放HashMap元素的實體
  • size:記錄了放入HashMap的元素個數
  • loadFactor:負載因子
  • threshold:閾值,決定了HashMap何時擴容,以及擴容後的大小,一般等於table大小乘以loadFactor

HashMap的構造函數

HashMap的構造函數主要有四個,代碼如下: [java] view plain copy
  1. public HashMap(int initialCapacity, float loadFactor) {
  2. ...
  3. this.loadFactor = loadFactor;
  4. this.threshold = tableSizeFor(initialCapacity);
  5. }
  6. public HashMap(int initialCapacity) {
  7. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  8. }
  9. public HashMap() {
  10. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  11. }
  12. public HashMap(Map<? extends K, ? extends V> m) {
  13. this.loadFactor = DEFAULT_LOAD_FACTOR;
  14. putMapEntries(m, false);
  15. }

其中主要有兩種形式:
  • 直接拷貝別的HashMap的形式,在此不作討論
  • 定義初始容量大小(table數組的大小,缺省值為16),定義負載因子(缺省值為0.75)的形式
值得註意的是,當我們自定義HashMap初始容量大小時,構造函數並非直接把我們定義的數值當做HashMap容量大小,而是把該數值當做參數調用方法tableSizeFor,然後把返回值作為HashMap的初始容量大小: [java] view plain copy
  1. /**
  2. * Returns a power of two size for the given target capacity.
  3. */
  4. static final int tableSizeFor(int cap) {
  5. int n = cap - 1;
  6. n |= n >>> 1;
  7. n |= n >>> 2;
  8. n |= n >>> 4;
  9. n |= n >>> 8;
  10. n |= n >>> 16;
  11. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  12. }

該方法會返回一個大於等於當前參數的2的倍數,因此HashMap中的table數組的容量大小總是2的倍數。

何時進行擴容?

HashMap使用的是懶加載,構造完HashMap對象後,只要不進行put 方法插入元素之前,HashMap並不會去初始化或者擴容table: [java] view plain copy
  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  5. boolean evict) {
  6. Node<K,V>[] tab; Node<K,V> p; int n, i;
  7. if ((tab = table) == null || (n = tab.length) == 0)
  8. n = (tab = resize()).length;
  9. if ((p = tab[i = (n - 1) & hash]) == null)
  10. tab[i] = newNode(hash, key, value, null);
  11. else {
  12. ...
  13. }
  14. ++modCount;
  15. if (++size > threshold)
  16. resize();
  17. afterNodeInsertion(evict);
  18. return null;
  19. }

在putVal方法第8、9行我們可以看到,當首次調用put方法時,HashMap會發現table為空然後調用resize方法進行初始化 在putVal方法第16、17行我們可以看到,當添加完元素後,如果HashMap發現size(元素總數)大於threshold(閾值),則會調用resize方法進行擴容 在這裏值得註意的是,在putVal方法第10行我們可以看到,插入元素的hash值是一個32位的int值,而實際當前元素插入table的索引的值為 : [java] view plain copy
  1. (table.size - 1)& hash

又由於table的大小一直是2的倍數,2的N次方,因此當前元素插入table的索引的值為其hash值的後N位組成的值

resize擴容

[java] view plain copy
  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. if (oldCap >= MAXIMUM_CAPACITY) {
  8. threshold = Integer.MAX_VALUE;
  9. return oldTab;
  10. }
  11. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  12. oldCap >= DEFAULT_INITIAL_CAPACITY)
  13. newThr = oldThr << 1; // double threshold
  14. }
  15. else if (oldThr > 0) // initial capacity was placed in threshold
  16. newCap = oldThr;
  17. else { // zero initial threshold signifies using defaults
  18. newCap = DEFAULT_INITIAL_CAPACITY;
  19. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  20. }
  21. if (newThr == 0) {
  22. float ft = (float)newCap * loadFactor;
  23. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  24. (int)ft : Integer.MAX_VALUE);
  25. }
  26. threshold = newThr;
  27. @SuppressWarnings({"rawtypes","unchecked"})
  28. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  29. table = newTab;
  30. if (oldTab != null) {
  31. for (int j = 0; j < oldCap; ++j) {
  32. Node<K,V> e;
  33. if ((e = oldTab[j]) != null) {
  34. oldTab[j] = null;
  35. if (e.next == null)
  36. newTab[e.hash & (newCap - 1)] = e;
  37. else if (e instanceof TreeNode)
  38. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  39. else { // preserve order
  40. Node<K,V> loHead = null, loTail = null;
  41. Node<K,V> hiHead = null, hiTail = null;
  42. Node<K,V> next;
  43. do {
  44. next = e.next;
  45. if ((e.hash & oldCap) == 0) {
  46. if (loTail == null)
  47. loHead = e;
  48. else
  49. loTail.next = e;
  50. loTail = e;
  51. }
  52. else {
  53. if (hiTail == null)
  54. hiHead = e;
  55. else
  56. hiTail.next = e;
  57. hiTail = e;
  58. }
  59. } while ((e = next) != null);
  60. if (loTail != null) {
  61. loTail.next = null;
  62. newTab[j] = loHead;
  63. }
  64. if (hiTail != null) {
  65. hiTail.next = null;
  66. newTab[j + oldCap] = hiHead;
  67. }
  68. }
  69. }
  70. }
  71. }
  72. return newTab;
  73. }

從第15 ~ 20行可以看到,若threshold(閾值)不為空,table的首次初始化大小為閾值,否則初始化為缺省值大小16 當table需要擴容時,從第11 ~ 13行可以看到,擴容後的table大小變為原來的兩倍,接下來就是進行擴容後table的調整: 假設擴容前的table大小為2的N次方,有上述put方法解析可知,元素的table索引為其hash值的後N位確定 那麽擴容後的table大小即為2的N+1次方,則其中元素的table索引為其hash值的後N+1位確定,比原來多了一位 因此,table中的元素只有兩種情況:
  1. 元素hash值第N+1位為0:不需要進行位置調整
  2. 元素hash值第N+1位為1:調整至原索引的兩倍位置
在resize方法中,第45行的判斷即用於確定元素hashi值第N+1位是否為0:
  • 若為0,則使用loHead與loTail,將元素移至新table的原索引處
  • 若不為0,則使用hiHead與hiHead,將元素移至新table的兩倍索引處
擴容或初始化完成後,resize方法返回新的table

Java HashMap的擴容