HashSet 扩容的底层机制说明

目录

1.扩容机制说明

 2.底层机制说明


1.扩容机制说明

扩容机制:

     HashSet的底层就是HashMap(底层是数组+链表/红黑树),当添加元素时先得到其hash值再转换成索引,找到存取数据的table,看这个位置是否已经存放了元素,如果没有,则直接存放,如果有,调用equals后看是否相同,如果不相同,则放在则添加到最后,相同则放弃添加。在Java8中一条链表的元素个数达到默认值8,并且table数组的长度大于等于64,则会进行树化。

扩容机制的细节:

1.HashSet的底层就是HashMap,第一次添加元素时,table数组会扩容到16,加载因子是0.75,临界值是16*0.75==12(当HashSet里的元素超过12时就会进行第二次扩容)。

2.每次扩容table都是上一次容量*2(第一次除外),临界值是扩容后的容量*0.75。

3.在Java8中一条链表的元素个数达到默认值8,并且table数组的长度大于等于64,则会进行树化。否则继续按照table扩容机制进行扩容。(每次扩容table都是上一次容量*2)


 2.底层机制说明

public class HashSetSource {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("java");//到此位置,第 1 次 add 分析完毕. hashSet.add("php");//到此位置,第 2 次 add 分析完毕
        hashSet.add("java");
        System.out.println("set=" + hashSet);

          //1. 执行 HashSet ()
//        public HashSet() {
//            map = new HashMap<>();
//        }
          //2. 执行 add ()
//        public boolean add (E e){//e = "java"
//            return map.put(e, PRESENT) == null;//(static) PRESENT = new Object();
//        }
          //3. 执行 put (), 该方法会执行 hash(key) 得到 key 对应的 hash 值 算法 h = key.hashCode()) ^(h >>> 16)
//        public V put (K key, V value){//key = "java" value = PRESENT 共享
//            return putVal(hash(key), key, value, false, true);
//        }
          //4. 执行 putVal
//        final V putVal ( int hash, K key, V value,boolean onlyIfAbsent, boolean evict){
//            Node<K, V>[] tab;
//            Node<K, V> p;
//            int n, i; //定义了辅助变量table 就是 HashMap 的一个数组,类型是 Node[]
                        // if 语句表示如果当前 table 是 null, 或者 大小=0
                        //就是第一次扩容,到 16 个空间. if ((tab = table) == null || (n = tab.length) == 0)
//            n = (tab = resize()).length;
             //(1)根据 key,得到 hash 去计算该 key 应该存放到 table 表的哪个索引位置并把这个位置的对象,赋给 p
             //(2)判断 p 是否为 null
             //(2.1) 如果 p 为 null, 表示还没有存放元素, 就创建一个 Node (key="java",value=PRESENT)
             //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null)
//            if ((p = tab[i = (n - 1) & hash]) == null)
//                tab[i] = newNode(hash, key, value, null);
//            else {
                   //如果当前索引位置对应的链表的第一个元素和准备添加的 key 的 hash 值一样
                   //并且满足 下面两个条件之一:
                   //(1) 准备加入的 key 和 p 指向的 Node 结点的 key 是同一个对象
                   //(2) p 指向的 Node 结点的 key 的 equals() 和准备加入的 key 比较后相同就不能加入
//            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
//                  e = p;
              //再判断 p 是不是一颗红黑树, //如果是一颗红黑树,就调用 putTreeVal , 来进行添加
//            else if (p instanceof TreeNode)
//                 e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
//            else {//如果 table 对应索引位置,已经是一个链表, 就使用 for 循环比较
                    //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
                    //注意在把元素添加到链表后,立即判断 该链表是否已经达到 8 个结点, 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
                    //注意,在转成红黑树时,要进行判断, 判断条件
//                    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
//                        resize();
                    //如果上面条件成立,先 table 扩容. // 只有上面条件不成立时,才进行转成红黑树
                    //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接 break
//                    for (int binCount = 0; ; ++binCount) {
//                        if ((e = p.next) == null) {
//                            p.next = newNode(hash, key, value, null);
//                            if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st
//                                treeifyBin(tab, hash);
//                            break;
//                        }
//                        if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
//                            break;
//                        p = e;
//                    }
//                }
//                if (e != null) { // existing mapping for key
//                    V oldValue = e.value;
//                    if (!onlyIfAbsent || oldValue == null)
//                        e.value = value;
//                    afterNodeAccess(e);
//                    return oldValue;
//                }
//            }
//            ++modCount;
              //size 就是我们每加入一个结点 Node(k,v,h,next), size++
//            if (++size > threshold)
//                resize();//扩容
//            afterNodeInsertion(evict);
//            return null;
//        }
//    }
    }
}

以上为我个人的小分享,如有问题,欢迎讨论!!! 

都看到这了,不如关注一下,给个免费的赞 

<think>嗯,用户专门询问了Java HashSet扩容机制,我需要结合站内引用内容和Java集合框架的知识来详细解答。用户之前提供的引用中,[2]和[3]尤其相关,它们提到了HashSet扩容触发条件和底层原理。 用户可能正在学习Java集合框架,或是遇到了性能优化问题。从ta明确要求"详解"来看,需要深度技术解析而非表面介绍。值得注意的是,引用[3]详细说明了HashMap扩容机制,而HashSet底层正是基于HashMap的——这点必须强调清楚。 关于扩容原理,需要分四部分组织答案:首先是必要性(为什么需要扩容),引用[1]提到"避免性能瓶颈"可支撑这点;其次是触发条件,引用[3]的负载因子计算(16*0.75=12)是核心;然后是扩容过程,要解释2倍扩容和元素迁移;最后是性能影响,引用[3]提到"扩容代价O(n)"这一点很关键。 特别注意两个技术细节:一是HashSet的无序性依赖于扩容后重哈希(引用[2]提到"保持元素分布均匀性"),二是构造函数的初始容量设置技巧(引用[4]的代码示例展示了容量监控方法)。用户可能不知道初始容量设置能避免多次扩容,这属于实用优化建议。 需要澄清一个潜在误区:引用[2]提到"equals()为false但hashCode()为true"的情况,这和扩容无关而是哈希碰撞问题,但为避免混淆应该在相关问题里提及。最后生成的问题要覆盖底层原理(如与HashMap关联性)和实战技巧(如监控实际容量)。 </think>### Java HashSet 扩容机制详解 #### **一、底层依赖关系** **HashSet 完全基于 HashMap 实现**,其内部使用一个 `HashMap<E, Object>` 存储元素(元素作为 Key,统一用 `PRESENT` 对象占位 Value)。因此 **HashSet扩容机制本质是 HashMap 的扩容机制** [^2][^3]。 #### **二、扩容核心参数** 1. **初始容量** - 默认 **16**(桶数量),可通过构造函数调整: ```java Set<String> set1 = new HashSet<>(); // 默认容量16 Set<String> set2 = new HashSet<>(32); // 指定初始容量32 ``` 2. **负载因子(Load Factor)** - 默认 **0.75**,表示当元素数量达到 `当前容量 × 0.75` 时触发扩容 [^3]。 #### **三、扩容触发条件** 当同时满足以下两点时触发扩容 [^3]: ```math 元素数量 > 当前容量 \times 负载因子 ``` 例如:默认容量 16,负载因子 0.75 → **元素数量超过 12 时扩容**。 #### **四、扩容执行过程** 1. **创建新数组** 容量扩大为 **原容量的 2 倍**(如 16 → 32)[^3]。 2. **重新哈希(Rehash)** - 遍历原数组的所有桶(Bucket) - 对每个元素 **重新计算哈希值**(`hashCode() & (newCapacity-1)`) - 将元素迁移到新数组的对应桶中 [^3][^4]。 3. **数据迁移完成** 旧数组被丢弃,后续操作基于新数组执行。 #### **五、性能影响与优化** | 阶段 | 时间复杂度 | 说明 | |---------------|------------|----------------------------------------------------------------------| | **扩容触发时** | $O(n)$ | 需遍历所有元素重新哈希和迁移,元素越多耗时越长 [^3] | | **扩容后查询** | $O(1)$ | 桶数量增加,哈希冲突减少,查询效率提升 | **优化建议**: ```java // 预分配足够容量避免多次扩容 Set<Integer> highPerfSet = new HashSet<>(预期元素数量 / 0.75 + 1); ``` > 例如预期存储 1000 个元素:初始容量 = $1000 \div 0.75 \approx 1333$,向上取整为 1334。 #### **六、实验验证** 通过反射查看实际容量变化 [^4]: ```java import java.lang.reflect.Field; import java.util.HashSet; public class HashSetCapacityTest { public static void main(String[] args) throws Exception { HashSet<Integer> set = new HashSet<>(); Field field = HashSet.class.getDeclaredField("map"); field.setAccessible(true); HashMap<Integer, ?> map = (HashMap<Integer, ?>) field.get(set); for (int i = 1; i <= 13; i++) { // 默认扩容阈值为12(16*0.75) set.add(i); System.out.println("元素: " + i + ", 桶数量: " + getCapacity(map)); // 添加第13个元素时触发扩容 } } // 获取HashMap底层数组长度 static int getCapacity(HashMap<?,?> map) throws Exception { Field tableField = HashMap.class.getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(map); return table == null ? 0 : table.length; } } ``` **输出示例**: ``` 元素: 1, 桶数量: 16 ... 元素: 12, 桶数量: 16 // 尚未触发扩容 元素: 13, 桶数量: 32 // 扩容发生! ``` #### **七、总结特点** 1. **无序性与扩容相关**:扩容时元素位置改变,导致遍历顺序变化 [^2]。 2. **线程不安全**:扩容期间并发操作可能导致数据丢失或死循环。 3. **空间换时间**:通过加倍扩容减少哈希冲突,维持 $O(1)$ 时间复杂度查询 [^3]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值