HashSet底层原理详解

HashSet底层原理详解

1. 说明

  1. HashSet实现了Set接口
  2. HashSet底层实质上是HashMap
  3. 可以存放null值,但是只能有一个null
  4. HashSet不保证元素是有序的,取决于hash后,再确定索引的结果,即不保证存放元素的顺序和取出顺序一致
  5. 不能有重复元素/对象

2. 底层机制说明

HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
  1. 先获取元素的哈希值(hashcode方法)
  2. 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
  3. 如果该位置上没有其他元素,则直接存放,如果该位置上有其他元素,则需要进行equals判断,如果相等,则不再添加,如果不相等,则以链表的方式添加
  4. Java8以后,如果一条链表中的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行数化(红黑树)
class HashSetSource {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        //第 1 次 add
        hashSet.add("java");
        //第 2 次 add
        hashSet.add("php");
        hashSet.add("java");
        System.out.println("set=" + hashSet);

        /**
         * 对 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 {
         *
         *         //一个开发技巧提示: 在需要局部变量(辅助变量)时候,在创建
         *
         *             Node<K,V> e; K k; 
         *
         *         //如果当前索引位置对应的链表的第一个元素和准备添加的 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

         */

    }
}

3. 分析HashSet的扩容和转成红黑树机制

  1. HashSet底层是HashMap,第一次添加时,table的数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor是0.75)=12
  2. 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,依次类推
  3. Java8以后,如果一条链表中的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行数化(红黑树),否则仍然采用数组扩容机制
posted @   mx_info  阅读(3167)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示