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值, 然后异或上他的右移十六位的值, 为的是防止冲突
- 获得hash值之后就会进入
putVal
方法, 这个方法较为庞大, 是核心方法
- 首先定义一些辅助变量
table
是放Node<>
节点的一个数组, 属于HashMap
- 如果当前
table
为空, 就会执行一个resize()
方法, 这也是一个比较庞大的方法, 这里不会过多赘述
大概作用就是扩容到16个空间, 并且里面还会根据加载因子(一般是0.75)来计算一个临界值threshold
- 根据传入的key的hash值来计算该key应该存放到
table
的哪个位置, 并且将这个位置赋值为p
如果这个值为空, 表示还没有存放过元素, 就要new一个新Node- 如果不为空, 就会进入else中
if : 当前索引位置对应的链表的第一个元素的hash值和传入的hash值一样 && (key一样 || equals()相同), 就将e 置为 p
else if : p是不是一颗红黑树, 如果是红黑树, 就调用putTreeVal
进行添加, 这里不再赘述
else : 这个时候就是一个链表, 并且表头和当前添加的对象不同, 这个时候就该遍历链表找有没有相同的对象
如果发现一个一样, 就会将e置为当前重复的对象, break, 否则就会走到最后, 将待添加的对象添加到尾部, e就为null
添加到链表后会判断当前链表的size, 如果大于8, 就会树化afterNodeInsertion
是一个空方法, 不需要管, 是为了让子类实现的- 返回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), 就会进行树化, 否则仍然采用数组的扩容机制
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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)