先说结论

对于一张容量为0的 HashMap,会先调用 resize() 方法建立新表,新表的容量为16,阈值为12。在插入元素之后,当检测到插入后的表超过了阈值大小,就会重新调用 resize() 方法对新表进行扩容,并把老表的值按照新表的尺寸依据 hash 插入原则赋值给新表。扩容的原则根据 tableSizeFor() 方法决定,一般来说,扩容会按照之前的2倍来扩容,并根据扩容后的大小和装填因子确认新阈值。

注意:当删除红黑树结点导致其形成某种结构的时候,会剪枝退化为链表。


源码解读

对于 HashSet 而言,其实现了 Set 接口,是 Set 的一种常用的实现类。它依据散列表的机制,对所有的元素进行整合存储,能够保证元素不会产生重复。与此同时,它也是一个可变长度的数据结构,可以根据元素量进行动态扩充,同时为了保证读写的高效性,也在底层实现上进行了一定的优化,形成了现在数组 + 链表 + 红黑树的实现结构。

HashSet 与 HashMap

查看 HashSet 定义代码可以看到,该类对象本质上就是对一个 HashMap 对象进行了包装,使其能够实现 Set 集合的各种方法。其字段定义部分如下所示:

  1. // 存储单元依然是 HashMap 对象
  2. private transient HashMap<E, Object> map;
  3. // 一个静态常量 Object 对象,没有实际意义,用来给 HashMap 中的value占位
  4. private static final Object PRESENT = new Object();

观察 HashSet 的众多方法也可以看出,许多方法的实现都需要依赖 HashMap 的方法,如:

  1. public boolean add(E e) {
  2. return this.map.put(e, PRESENT) == null;
  3. }

因此,对 HashSet 内部存储机制的解读,本质上就是对 HashMap 存储机制的解读。

HashMap 存储机制

HashMap 的字段显示,其存储实现是一个 HashMap.Node 类的对象数组。可以查看 HashMap 类中的字段声明来加深理解。

  1. private static final long serialVersionUID = 362498820763181265L;
  2. // 默认的最初容量值为16
  3. static final int DEFAULT_INITIAL_CAPACITY = 16;
  4. // HashMap 对象最大容量值
  5. static final int MAXIMUM_CAPACITY = 1073741824;
  6. // 默认的装填因子为0.75
  7. static final float DEFAULT_LOAD_FACTOR = 0.75F;
  8. // 红黑树化阈值为8,当拉链长度达到这个值时,达到红黑树化条件一
  9. static final int TREEIFY_THRESHOLD = 8;
  10. // 不红黑树化阈值为6
  11. static final int UNTREEIFY_THRESHOLD = 6;
  12. // 最小树化容量为64,当表容量达到这个值时,达到红黑树化条件二
  13. static final int MIN_TREEIFY_CAPACITY = 64;
  14. // 元素结点的底层存储为一个 HashMap.Node 类的对象数组
  15. transient HashMap.Node<K, V>[] table;
  16. // Set 包装后的 HashMap 对象,该对象可以使得 HashMap 对象当作 Set 使用
  17. transient Set<Entry<K, V>> entrySet;
  18. // HashMap 实际元素数量
  19. transient int size;
  20. // 操作计数,与多线程和线程同步有关
  21. transient int modCount;
  22. // 当前阈值,用于初始化时指定容量的情况
  23. int threshold;
  24. // 装填因子,用于初始化时指定装填因子的情况
  25. final float loadFactor;

装填因子与默认装填因子

HashMap 可以用带参和无参构造器构造对象,Java 给出了指定 HashMap 装填因子及阈值的方法,如下所示:

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. if (initialCapacity < 0) {
  3. throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  4. } else {
  5. if (initialCapacity > 1073741824) {
  6. initialCapacity = 1073741824;
  7. }
  8. if (!(loadFactor <= 0.0F) && !Float.isNaN(loadFactor)) {
  9. this.loadFactor = loadFactor;
  10. this.threshold = tableSizeFor(initialCapacity);
  11. } else {
  12. throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
  13. }
  14. }
  15. }
  16. public HashMap(int initialCapacity) {
  17. this(initialCapacity, 0.75F);
  18. }
  19. public HashMap() {
  20. this.loadFactor = 0.75F;
  21. }
  22. static final int tableSizeFor(int cap) {
  23. int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
  24. return n < 0 ? 1 : (n >= 1073741824 ? 1073741824 : n + 1);
  25. }
  26. public static int numberOfLeadingZeros(int i) {
  27. if (i <= 0) {
  28. return i == 0 ? 32 : 0;
  29. } else {
  30. int n = 31;
  31. if (i >= 65536) {
  32. n -= 16;
  33. i >>>= 16;
  34. }
  35. if (i >= 256) {
  36. n -= 8;
  37. i >>>= 8;
  38. }
  39. if (i >= 16) {
  40. n -= 4;
  41. i >>>= 4;
  42. }
  43. if (i >= 4) {
  44. n -= 2;
  45. i >>>= 2;
  46. }
  47. return n - (i >>> 1);
  48. }
  49. }

可以看出,调用有参的构造器时,HashMap 会初始化表的装填因子,并按照一定的策略计算得到表的大小,无论如何都不会超过最大容量值。而对于无参的构造器,则会直接将装填因子默认设置为0.75,且不指定初始容量。

hash()

在讨论 put() 方法之前先讨论 hash() 方法,该方法是用来求得 HashMap 中元素的 hash 值的,从源码中不难看出这个 hash 值与 Object 类中定义的 hashCode() 并不一样,但具有相当的关系。当 key 为 null 值的时候,hash 值固定为空,此方法将用于后续的 put() 方法。

  1. static final int hash(Object key) {
  2. int h;
  3. return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;
  4. }

put()

此处假设创建对象的时候采用的是无参的构造器,以此来查看其扩容机制。由 HashSet 中的 add() 方法可以看出,其通过调用 map 对象的 put() 方法实现元素的添加。put() 方法及其依赖的 putVal() 方法展示如下:

  1. public V put(K key, V value) {
  2. return this.putVal(hash(key), key, value, false, true);
  3. }
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  5. HashMap.Node[] tab;
  6. int n;
  7. // 判断 HashMap 中的 table 是否为 null 或者为空表
  8. // 如果是就调用 resize() 方法生成一个新表并赋值给临时变量 tab
  9. if ((tab = this.table) == null || (n = tab.length) == 0) {
  10. n = (tab = this.resize()).length;
  11. }
  12. Object p;
  13. int i;
  14. // 判断 tab 表中 (n - 1) & hash 对应的索引是否为空,如果是就把这个结点挂载到对应的位置
  15. if ((p = tab[i = n - 1 & hash]) == null) {
  16. tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
  17. // 如果索引不是空,进行查重判断
  18. } else {
  19. Object e;
  20. Object k;
  21. // 变量 p 是 table 该索引中的那个变量
  22. // 如果当前索引位置的第一个结点的 hash 相同且 key 相同
  23. // key 相同的条件:为同一个对象,或者在 equals() 的意义上相等
  24. // 就把原来那个变量 p 赋值给 e
  25. if (((HashMap.Node)p).hash == hash &&
  26. ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
  27. e = p;
  28. // 判断变量 p 是不是一棵红黑树,如果是就采用红黑树的方式进行比较和添加元素
  29. } else if (p instanceof HashMap.TreeNode) {
  30. e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
  31. // 拉链为链表的情况下的处理方式
  32. } else {
  33. int binCount = 0;
  34. while(true) {
  35. // 加到最后面
  36. if ((e = ((HashMap.Node)p).next) == null) {
  37. ((HashMap.Node)p).next =
  38. this.newNode(hash, key, value, (HashMap.Node)null);
  39. // 添加到第8个元素后,就将链表红黑树化
  40. if (binCount >= 7) {
  41. this.treeifyBin(tab, hash);
  42. }
  43. break;
  44. }
  45. // 查到重复的了,就跳出循环
  46. if (((HashMap.Node)e).hash == hash &&
  47. ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
  48. break;
  49. }
  50. p = e;
  51. ++binCount;
  52. }
  53. }
  54. // 如果 e 不为 null,就代表没有插入元素,对于 HashMap 而言需要更新键值对中的值
  55. if (e != null) {
  56. V oldValue = ((HashMap.Node)e).value;
  57. if (!onlyIfAbsent || oldValue == null) {
  58. ((HashMap.Node)e).value = value;
  59. }
  60. this.afterNodeAccess((HashMap.Node)e);
  61. return oldValue;
  62. }
  63. }
  64. ++this.modCount;
  65. // 当前表的容量大于阈值的时候,就对表进行扩容
  66. if (++this.size > this.threshold) {
  67. this.resize();
  68. }
  69. // 在 HashMap 中为一个空方法,方便子类实现更高级的功能
  70. this.afterNodeInsertion(evict);
  71. // 返回 null 代表添加成功
  72. return null;
  73. }
  74. final HashMap.Node<K, V>[] resize() {
  75. HashMap.Node<K, V>[] oldTab = this.table;
  76. // 得到之前表格的大小
  77. int oldCap = oldTab == null ? 0 : oldTab.length;
  78. // 得到之前表格的阈值
  79. int oldThr = this.threshold;
  80. // 新表的阈值
  81. int newThr = 0;
  82. // 新表的容量
  83. int newCap;
  84. if (oldCap > 0) {
  85. if (oldCap >= 1073741824) {
  86. this.threshold = 2147483647;
  87. return oldTab;
  88. }
  89. if ((newCap = oldCap << 1) < 1073741824 && oldCap >= 16) {
  90. newThr = oldThr << 1;
  91. }
  92. // 如果一开始表为空表但阈值不为0,就按照阈值设置新表大小
  93. } else if (oldThr > 0) {
  94. newCap = oldThr;
  95. } else {
  96. // 当老表的阈值和容量都为0的时候,就用把新表容量设置为16,阈值设置为12
  97. newCap = 16;
  98. newThr = 12;
  99. }
  100. if (newThr == 0) {
  101. float ft = (float)newCap * this.loadFactor;
  102. newThr = newCap < 1073741824 && ft < 1.07374182E9F ? (int)ft : 2147483647;
  103. }
  104. // 新表阈值赋值给阈值字段
  105. this.threshold = newThr;
  106. // 按照新表容量创建新表
  107. HashMap.Node<K, V>[] newTab = new HashMap.Node[newCap];
  108. this.table = newTab;
  109. // 把老表的内容复制到新表中去
  110. if (oldTab != null) {
  111. for(int j = 0; j < oldCap; ++j) {
  112. HashMap.Node e;
  113. if ((e = oldTab[j]) != null) {
  114. oldTab[j] = null;
  115. if (e.next == null) {
  116. newTab[e.hash & newCap - 1] = e;
  117. } else if (e instanceof HashMap.TreeNode) {
  118. ((HashMap.TreeNode)e).split(this, newTab, j, oldCap);
  119. } else {
  120. HashMap.Node<K, V> loHead = null;
  121. HashMap.Node<K, V> loTail = null;
  122. HashMap.Node<K, V> hiHead = null;
  123. HashMap.Node hiTail = null;
  124. HashMap.Node next;
  125. do {
  126. next = e.next;
  127. if ((e.hash & oldCap) == 0) {
  128. if (loTail == null) {
  129. loHead = e;
  130. } else {
  131. loTail.next = e;
  132. }
  133. loTail = e;
  134. } else {
  135. if (hiTail == null) {
  136. hiHead = e;
  137. } else {
  138. hiTail.next = e;
  139. }
  140. hiTail = e;
  141. }
  142. e = next;
  143. } while(next != null);
  144. if (loTail != null) {
  145. loTail.next = null;
  146. newTab[j] = loHead;
  147. }
  148. if (hiTail != null) {
  149. hiTail.next = null;
  150. newTab[j + oldCap] = hiHead;
  151. }
  152. }
  153. }
  154. }
  155. }
  156. // 返回新表
  157. return newTab;
  158. }

可以看到,对于一张容量为0的 HashMap,会先调用 resize() 方法建立新表,新表的容量为16,阈值为12。在插入元素之后,当检测到插入后的表超过了阈值大小,就会重新调用 resize() 方法对新表进行扩容,并把老表的值按照新表的尺寸依据 hash 插入原则赋值给新表。

插入过程

上节中源码也注释了插入元素的过程,即现根据插入元素的 hash 值计算出应该放入哪个索引中,然后对该索引进行判断:

  1. 若该索引中为空值,就直接把元素存储到索引中;
  2. 若该索引中不为空值,且已经是一个红黑树结点,就利用 putTreeVal() 方法在该红黑树中查重和插入;
  3. 若该索引中不为控制且只是一个链表结点,就依次判断每个结点是否产生重复,若没有重复就在最后插入,插入后立即判断是否达到红黑树建立条件(某个索引链表长度达到8且 table 容量达到64),若满足则将该索引处的结点红黑树化,否则继续扩容,若有重复,就根据 HashMap 的操作法则对 Value 值进行更新。

onlyIfAbsent 参数用于控制 key 出现重复后是否更新 value,默认是更新的,即为 false。HashMap 的其他子类通过 AfterNodeAccess() 和 AfterNodeInsertion() 方法实现结点处理后的高级功能。

注意:当删除红黑树结点导致其形成某种结构的时候,会剪枝退化为链表。