HashSet底层原理详解
HashSet底层原理详解
1. 说明
- HashSet实现了Set接口
- HashSet底层实质上是HashMap
- 可以存放null值,但是只能有一个null
- HashSet不保证元素是有序的,取决于hash后,再确定索引的结果,即不保证存放元素的顺序和取出顺序一致
- 不能有重复元素/对象
2. 底层机制说明
HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
- 先获取元素的哈希值(hashcode方法)
- 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
- 如果该位置上没有其他元素,则直接存放,如果该位置上有其他元素,则需要进行equals判断,如果相等,则不再添加,如果不相等,则以链表的方式添加
- 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的扩容和转成红黑树机制
- HashSet底层是HashMap,第一次添加时,table的数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor是0.75)=12
- 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,依次类推
- Java8以后,如果一条链表中的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行数化(红黑树),否则仍然采用数组扩容机制
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 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)