Java中HashSet解读以及底层源码

HashSet

Set接口说明

1). Set 接口是 Collection 的子接口, Set 接口的实现类不能存放重复的元素
可以添加null, 且存放的次序是无序的(存放和取出的顺序不一致)
注意: 取出的顺序虽然不是添加的顺序, 但是是固定的

2). 可以利用迭代器遍历, 但是不能使用下标索引遍历

HashSet说明

1). HashSet底层其实是HashMap

2). add()HashSet 添加元素的方法, 会返回一个Boolean, 成功后返回true, 失败返回false, 其实失败就是有重复元素
这边重复其实是按照底层分析的
例:

class Cat {
    private String name;

    public Cat(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                '}';
    }
}

public class Main {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();

        hashSet.add("jack"); // 成功
        hashSet.add("jack"); // 失败

        hashSet.add(new Cat("tom")); // 成功
        hashSet.add(new Cat("tom")); // 成功

        hashSet.add(new String("111")); // 成功
        hashSet.add(new String("111")); // 失败

        System.out.println(hashSet); //[111, Cat{name='tom'}, Cat{name='tom'}, jack]

    }
}

这里String 有一个坑, 留待源码解读部分解释

HashSet扩容机制

结论

1). HashSet 底层是 HashMap
2). 添加一个元素时, 先得到hash值, 然后会转成索引值
3). 找到存储数组表Table, 看这个索引位置是否存在元素
4). 如果没有就直接加入, 如果有, 就会调用equals() 比较, 如果相同就会放弃添加, 如果不相同, 就添加到最后
5). 在Java8中, 如果一条链表的元素个数超过默认值(8), 并且Table大小 >= 默认值(64), 就会进行树化

源码解读

代码

public class Main {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();

        hashSet.add("java");
        hashSet.add("php");
        hashSet.add("java");

        System.out.println(hashSet);
    }
}

1). 首先进入构造器, 底层是一个HashMap

2). 随后是add() 方法

这时e = "java"

3). 然后调用map的put方法

这里可以发现传入了两个值, key和Value, 这里key就是当前要加的字符串"java", 而Value就是之前放入的PRESENT,
这里是HashSet 为了使用HashMap 而设置的一个final static类型的Object对象, 用来占位, 没有实际意义

所以之后不管add多少次, Value都不会变

4). 这里首先看hash方法

可以发现这里是先求出来key的hashCode值, 然后异或上他的右移十六位的值, 为的是防止冲突

  1. 获得hash值之后就会进入putVal 方法, 这个方法较为庞大, 是核心方法

  1. 首先定义一些辅助变量
  2. table 是放Node<> 节点的一个数组, 属于HashMap
  3. 如果当前table为空, 就会执行一个resize() 方法, 这也是一个比较庞大的方法, 这里不会过多赘述
    大概作用就是扩容到16个空间, 并且里面还会根据加载因子(一般是0.75)来计算一个临界值threshold
  4. 根据传入的key的hash值来计算该key应该存放到table的哪个位置, 并且将这个位置赋值为p
    如果这个值为空, 表示还没有存放过元素, 就要new一个新Node
  5. 如果不为空, 就会进入else中
    if : 当前索引位置对应的链表的第一个元素的hash值和传入的hash值一样 && (key一样 || equals()相同), 就将e 置为 p
    else if : p是不是一颗红黑树, 如果是红黑树, 就调用putTreeVal 进行添加, 这里不再赘述
    else : 这个时候就是一个链表, 并且表头和当前添加的对象不同, 这个时候就该遍历链表找有没有相同的对象
    如果发现一个一样, 就会将e置为当前重复的对象, break, 否则就会走到最后, 将待添加的对象添加到尾部, e就为null
    添加到链表后会判断当前链表的size, 如果大于8, 就会树化
  6. afterNodeInsertion 是一个空方法, 不需要管, 是为了让子类实现的
  7. 返回null代表成功, 返回的如果是一个对象, 那么就说明这个对象重复

扩容机制补充

1). 第一次添加时, table 扩容到16, 临界值threshold是16 * 加载因子LoadFactor(一般是0.75) = 12
2). 如果table数组 使用到临界值12, 就会扩容到16 * 2 = 32, 新的临界值就是32 * 0.75 = 24, 以此类推
3). 在Java8中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8), 并且table.length >= MIN_TREEIFY_CAPACITY(默认是64), 就会进行树化, 否则仍然采用数组的扩容机制

posted @   Xingon2356  阅读(19)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示