• 集合_HashSet(HashMap)扩容机制源码简析


    先说明一个问题:为什么说分析HashSet实际上是在分析HashMap?

            因为HashSet的构造方法,本质上都是调用HashMap的构造方法,对其内部维护的HashMap对象map进行初始化

    1. private transient HashMap map;
    2. public HashSet() {
    3. map = new HashMap<>();
    4. }
    5. public HashSet(Collection c) {
    6. map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    7. addAll(c);
    8. }
    9. public HashSet(int initialCapacity, float loadFactor) {
    10. map = new HashMap<>(initialCapacity, loadFactor);
    11. }
    12. public HashSet(int initialCapacity) {
    13. map = new HashMap<>(initialCapacity);
    14. }

            众所周知HashMap的存储方式为K-V键值对,而HashSet的存储方式从表面上来看为仅存储key,其原因是HashSet中存在PRESENT属性,该属性为Object类型,其作用为:作为K-V键值对中的value。所以实际上HashSet的存储方式也为K-V键值对,但value恒为PRESENT(通过HashMap实现,必然是存储键值对的)

            在HashMap中,value是可重复的,而key是不可重复的,该特性在HashSet上体现为"元素不可重复"(HashSet添加元素时只要求传入key)

    private static final Object PRESENT = new Object();

    扩容机制源码简析 

            调用HashSet的add方法,内部调用HashMap的put方法,put方法内部调用putVal方法

    1. public boolean add(E e) {
    2. return map.put(e, PRESENT)==null;
    3. }
    4. public V put(K key, V value) {
    5. return putVal(hash(key), key, value, false, true);
    6. }

            为了可读性,对于代码的解读写在注释中

            关于putVal方法

    1. //元素重复指的是key重复 即传入的key与链表中的key存在重复 发生重复时将放弃添加
    2. //若遍历完链表都不存在重复的key 则将key与value作为参数(还有hash和next)创建结点 链接在链表尾结点(1.8之前是链接在头结点)
    3. //而value是允许重复的 如HashSet中value恒为PRESENT
    4. //size指的是HashSet(HashMap)集合中的元素个数 而不是集合内部维护的数组存放了元素的索引的个数
    5. //例如 table.length == 16 只在索引0处存在一个长度为4的链表 其他索引处为空 此时size == 4而不是1
    6. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    7. boolean evict) {
    8. //定义辅助变量
    9. // tab指向table
    10. // n=table.length
    11. // p最初指向链表的头结点(此次准备添加的元素的hash对应的索引处的链表)
    12. // i=索引
    13. //table是HashMap中用于存储数据的数组 类型为Node 即数组中的元素都是链表(若有元素的话)
    14. HashMap.Node[] tab; HashMap.Node p; int n, i;
    15. //若table == null 或 table.length == 0 则进行第一次扩容(扩容为16)
    16. //扩容后用n重新记录扩容后的table长度
    17. if ((tab = table) == null || (n = tab.length) == 0)
    18. n = (tab = resize()).length;
    19. //n-1按位与hash(该hash不是hashCode 是hashCode经过按位异或无符号右移十六位的结果)计算得出索引 将该索引对应的链表的头结点赋值给p
    20. //若n=16 即n=2^4 则(n-1)&hash表示取hash二进制数的后四位 而四位二进制数的表示范围为0-15 覆盖了数组当前的所有索引
    21. //若p == null 即头结点为空 则代表此处尚未存储元素 将在该索引下创建一个新的结点作为头结点(参数为hash、key、value(PRESENT)、next)
    22. if ((p = tab[i = (n - 1) & hash]) == null)
    23. //newNode方法的作用为返回一个新建结点
    24. tab[i] = newNode(hash, key, value, null);
    25. //hash对应的索引不为空时 即存在链表时
    26. else {
    27. //定义辅助变量
    28. HashMap.Node e; K k;
    29. //若头结点的key的hash与准备添加的元素(后简称元素)的key的hash相等(hashCode相等)
    30. //且头结点的key与元素的key相等或元素的key不为null且头结点的key与元素的key经equals判断相等
    31. //这边说明一点 虽然p.hash与hash经计算得出的索引相同 但是不能想当然地认为p.hash == hash 因为决定索引的是二者的二进制数的后n位(2^n == 容量)
    32. //该if语句等价于 重复元素无法添加 除非hash相等且equals判断为true
    33. //所以hashCode与equals的重写总是成双成对的(设计者设计的规则 或许有其深意 但具体是什么我不清楚)
    34. //此处举例String类 Sting类重写了hashCode方法与equals方法
    35. //对于String类而言 两个String对象只要内容相同 则计算得出的hashCode就相同 且equals判断为true 即使这两个对象都是通过new创建的(均不指向字符串常量池)
    36. //此时对于内容相同的两个String对象 同时符合一、三判断 故新元素不能被添加
    37. //若此时存在一个类 只重写了hashCode方法或equals方法 该类的两个对象就无法同时通过一、三判断 即可以被添加
    38. if (p.hash == hash &&
    39. ((k = p.key) == key || (key != null && key.equals(k))))
    40. //将p的引用赋值给e 此时e != null 将走放弃添加的方法
    41. e = p;
    42. //判断链表是否为一颗红黑树 若是则调用putTreeVal方法添加元素
    43. else if (p instanceof TreeNode)
    44. e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
    45. //进行元素添加
    46. else {
    47. //遍历链表
    48. for (int binCount = 0; ; ++binCount) {
    49. //若一直遍历至尾结点都未找到相同元素 则进行添加 并break
    50. //此时e == null
    51. if ((e = p.next) == null) {
    52. //newNode方法的作用为返回一个新建结点
    53. p.next = newNode(hash, key, value, null);
    54. //添加元素时 链表的长度>=8 将进行树化(即添加完元素后 链表长度为9时 将进行树化)
    55. //但还需要满足一个条件 table.length >= MIN_TREEIFY_CAPACITY(默认为64) 否则不进行树化 而是进行扩容
    56. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    57. treeifyBin(tab, hash);
    58. break;
    59. }
    60. //若遍历时检测到存在相同元素 则放弃添加 并break
    61. if (e.hash == hash &&
    62. ((k = e.key) == key || (key != null && key.equals(k))))
    63. break;
    64. //等价于 p = p.next
    65. p = e;
    66. }
    67. }
    68. //若放弃添加 则e必然不等于null(元素重复) 即必然走该方法
    69. //该方法返回e.value e指向发生元素重复的结点(HashSet通过HashMap实现 其value恒为PRESENT)
    70. if (e != null) { // existing mapping for key
    71. V oldValue = e.value;
    72. //HashMap中的putVal方法的onlyIfAbsent参数为false 故该判断恒成立
    73. //即使在HashSet中value != null恒成立(value != null恒成立代表key发生重复即放弃添加)
    74. //故重复元素中的新元素的value必将覆盖旧元素的value
    75. //这代表着什么呢?
    76. //HashMap中数据是以键值对的方式存在的
    77. //key发生重复时不添加新的结点 但是新元素的value将覆盖旧元素的value
    78. //此为"修改" 即修改指定key值对应的结点的value
    79. if (!onlyIfAbsent || oldValue == null)
    80. e.value = value;
    81. //该方法交由HashMap的子类实现 在HashMap中是一个空方法
    82. afterNodeAccess(e);
    83. //即使oldValue == null 但也与 return null 是不同的
    84. return oldValue;
    85. }
    86. }
    87. ++modCount;
    88. //判断添加该元素时(注意是++size 也就是说size不受此次添加的元素的影响)数组中存储的元素是否已经超越了临界值threshold
    89. //若超过临界值threshold将进行扩容
    90. if (++size > threshold)
    91. resize();
    92. //该方法交由HashMap的子类实现 在HashMap中是一个空方法
    93. afterNodeInsertion(evict);
    94. //返回null代表添加成功
    95. return null;
    96. }

             关于扩容函数resize

    1. //扩容
    2. //简而言之:数组容量在第一次扩容时将扩容至16 或通过构造函数指定的容量(会经过tableSizeFor方法处理 暂存于临界值threshold) 后续扩容将扩容至先前容量的两倍
    3. //加载因子DEFAULT_LOAD_FACTOR(默认为0.75) 加载因子可自定义 即通过构造函数为loadFactory赋值 加载因子*容量=临界值
    4. //临界值会随着数组的扩容而扩容 系数同样为2(大多数情况下)
    5. //size超过临界值时表示数组需要进行扩容的(size等于临界值时不会)
    6. @SuppressWarnings("ALL")
    7. final HashMap.Node[] resize() {
    8. HashMap.Node[] oldTab = table;
    9. int oldCap = (oldTab == null) ? 0 : oldTab.length;
    10. int oldThr = threshold;
    11. int newCap, newThr = 0;
    12. //数组存在容量 换言之数组不是初始态数组
    13. if (oldCap > 0) {
    14. //若数组容量达到MAXIMUM_CAPACITY 将临界值threshold赋值为Integer.MAX_VALUE 即不再扩容
    15. if (oldCap >= MAXIMUM_CAPACITY) {
    16. threshold = Integer.MAX_VALUE;
    17. return oldTab;
    18. }
    19. //反之则二倍扩容
    20. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    21. oldCap >= DEFAULT_INITIAL_CAPACITY)
    22. newThr = oldThr << 1; // double threshold
    23. }
    24. //此处标记 后文会用到
    25. //若数组是初始态数组 但通过构造函数指定了容量(经过tableSizeFor方法处理后暂存于临界值threshold) 则将容量初始化为临界值
    26. else if (oldThr > 0) // initial capacity was placed in threshold
    27. newCap = oldThr;
    28. //若数组是初始态数组 但未通过构造函数指定临界值 则将数组容量初始化为16 并通过DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY计算临界值
    29. else { // zero initial threshold signifies using defaults
    30. newCap = DEFAULT_INITIAL_CAPACITY;
    31. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    32. }
    33. //以上的情况中 只有走标记处分支的情况下 newThr == 0才会成立
    34. //该分支用于为临界值threshold赋真正的值做准备(暂存于newThr)
    35. //若经标记处分支初始化的容量
    36. //反之将Integer.MAX_VALUE作为新临界值赋值给newThr
    37. if (newThr == 0) {
    38. float ft = (float)newCap * loadFactor;
    39. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    40. (int)ft : Integer.MAX_VALUE);
    41. }
    42. //将新临界值newThr赋值给临界值变量threshold
    43. threshold = newThr;
    44. //开始扩容
    45. @SuppressWarnings({"rawtypes","unchecked"})
    46. HashMap.Node[] newTab = (HashMap.Node[])new HashMap.Node[newCap];
    47. table = newTab;
    48. if (oldTab != null) {
    49. for (int j = 0; j < oldCap; ++j) {
    50. HashMap.Node e;
    51. if ((e = oldTab[j]) != null) {
    52. oldTab[j] = null;
    53. if (e.next == null)
    54. newTab[e.hash & (newCap - 1)] = e;
    55. else if (e instanceof HashMap.TreeNode)
    56. ((HashMap.TreeNode)e).split(this, newTab, j, oldCap);
    57. else { // preserve order
    58. HashMap.Node loHead = null, loTail = null;
    59. HashMap.Node hiHead = null, hiTail = null;
    60. HashMap.Node next;
    61. do {
    62. next = e.next;
    63. if ((e.hash & oldCap) == 0) {
    64. if (loTail == null)
    65. loHead = e;
    66. else
    67. loTail.next = e;
    68. loTail = e;
    69. }
    70. else {
    71. if (hiTail == null)
    72. hiHead = e;
    73. else
    74. hiTail.next = e;
    75. hiTail = e;
    76. }
    77. } while ((e = next) != null);
    78. if (loTail != null) {
    79. loTail.next = null;
    80. newTab[j] = loHead;
    81. }
    82. if (hiTail != null) {
    83. hiTail.next = null;
    84. newTab[j + oldCap] = hiHead;
    85. }
    86. }
    87. }
    88. }
    89. }
    90. return newTab;
    91. }

            关于树化函数treeifyBin

    1. //树化
    2. final void treeifyBin(HashMap.Node[] tab, int hash) {
    3. int n, index; HashMap.Node e;
    4. //table == null 或 table.length < MIN_TREEIFY_CAPACITY(默认为64)时 将先进行扩容 而不是树化
    5. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    6. resize();
    7. else if ((e = tab[index = (n - 1) & hash]) != null) {
    8. HashMap.TreeNode hd = null, tl = null;
    9. do {
    10. HashMap.TreeNode p = replacementTreeNode(e, null);
    11. if (tl == null)
    12. hd = p;
    13. else {
    14. p.prev = tl;
    15. tl.next = p;
    16. }
    17. tl = p;
    18. } while ((e = e.next) != null);
    19. if ((tab[index] = hd) != null)
    20. hd.treeify(tab);
    21. }
    22. }

            关于初始容量处理函数tableSizeFor

            该函数的作用为:返回大于或等于传入的初始容量的最小2的n次方数

            关于tableSizeFor可参照:

            集合_HashMap_tableSizeFor_Mudrock__的博客-CSDN博客

    1. static final int tableSizeFor(int cap) {
    2. int n = cap - 1;
    3. n |= n >>> 1;
    4. n |= n >>> 2;
    5. n |= n >>> 4;
    6. n |= n >>> 8;
    7. n |= n >>> 16;
    8. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    9. }
  • 相关阅读:
    勒索软件安全防护手册
    Reflex WMS中阶系列9 - Pick Run之前预留托盘号给备货订单
    简单的几个递归小算法
    【C语言数据结构——————排序(1万字)】
    js的静态方法和静态属性
    Vue项目中组件如何使用
    ActivityPub 笔记
    提升,方法比努力重要!不懂这几点再多的努力也白费!
    Swagger使用
    QQ隐藏福利二-----------------那些免费的挂件和气泡
  • 原文地址:https://blog.csdn.net/Mudrock__/article/details/126936098