# 本质剖析 为什么要使用HashSet
单列集合 - HashSet
特点一:去重与遍历
-
支持数据去重,可以使用 迭代器 或 foreach 遍历数据。
两种遍历方式的比较
-
迭代器遍历
- 通过调用 实现了
Iterable<T>接口的Iterator<T> iterator();方法, 从而获取迭代器对象,逐一访问元素。 - 优点:支持在遍历过程中安全地删除元素,避免并发修改异常。
- 适用场景:对集合进行删除操作时推荐使用。
示例代码:
Iterator<String> ite = collection.iterator(); while (ite.hasNext()) { String e = iterator.next(); // 遍历或处理元素 if (条件) { iterator.remove(); // 安全删除 } } - 通过调用 实现了
-
foreach遍历- 简洁明了,
foreach直接访问集合中的每个元素。 - 优点:代码更简洁,适合仅需要 读取 或 简单操作 元素的场景。
- 限制:不支持在遍历过程中直接
删除元素,会抛出ConcurrentModificationException。
示例代码:
for (String e : collection) { // 遍历或处理元素 } - 简洁明了,
选择建议
-
删除元素:使用 迭代器遍历。
-
仅访问或修改元素:使用 foreach 遍历,代码更简洁。
- 底层基于 哈希表(数组 + 链表)的存储方式,增删查改效率高。
foreach 和
iterator()的区别foreach 的底层依赖
Iteratorforeach循环是由 编译器生成代码 时 自动替换 为iterator的调用。这使得所有实现了Iterable接口的集合都可以使用foreach。
虽然底层机制相同,但
foreach和 显式使用iterator在功能和使用场景上有所不同:特性 foreach iterator 便捷性 代码更简洁,无需显式创建 Iterator对象必须手动创建 Iterator并控制遍历过程删除元素 不支持在遍历过程中删除元素(会抛出 ConcurrentModificationException)通过 iterator.remove()可安全删除元素灵活性 仅适合读取和简单操作元素 更灵活,可在遍历时进行复杂的集合操作 适用场景 适合只读操作,无需更改集合结构 适合需要动态修改集合的场景 -
特点二:定位查询
数据存储无序,但有规则
-
无序性:
HashSet存储过程中看似无序,是因为其底层并不对进行顺序维护,元素的位置是由 哈希算法 动态计算决定的。 -
规则性
尽管
HashSet中的元素存储是无序的,但它依赖哈希表的机制,遵循以下规则:-
元素的存储位置通过 哈希值(hashCode) 计算得出,决定它在哈希表中的索引。
什么是哈希值?
哈希值是由Java 中任意类的实例(如字符串、数字、自定义类等)的hashCode()方法生成的整数,用于快速定位数据在哈希表中的存储位置。-
意义
-
快速检索:减少查找复杂度,从 O(n) 降至 O(1)。
啥意思
即将逐一遍历提升为直接定位。
-
-
-
同样的对象始终会生成
一致的存储位置,因此尽管看似无序,但只要哈希值计算逻辑一致,其存储顺序也是可预测的。- 什么是一致性?
哈希表运作的基本规则之一, 即两个对象通过equals()方法比较相等时,哈希值也必须相同(通过hashCode()方法计算)。
- 什么是一致性?
-
哈希表的存储规则
索引唯一性
- 在哈希表中,通过哈希函数计算得到的 哈希值 被放到一个索引位置。
- 意味着:
每个索引位置在哈希表中是独立的,每个索引表示一个“桶”(bucket)。
一个桶可以包含一个元素,也可以包含多个冲突元素(通过链表或红黑树)。
哈希表如何存储元素?
-
哈希表的核心是“键值对”,严格来说,HashSet 并不是键值对结构,但其底层实现依赖于 HashMap,因此可以看作是通过键值对的方式间接实现的。它将键的哈希值转化为表的索引,用于存储对应的值:
-
哈希值计算:对键调用
hashCode(),计算其哈希值。 -
存储方式
- 如果索引位置为空,则直接将元素存储到该索引的桶中。
- 如果索引位置已有元素(哈希冲突),则将元素放入对应桶的链表或红黑树中。
例:
使用哈希表,用来存储人的名字和电话号码:
键:名字
"zs" 值:电话号码
"1234567890"- 计算哈希值:
"zs".hashCode()结果为2307740(假设)。 - 转化为索引:
使用哈希表大小(假设为 100),计算索引:index = 2307740 % 100 = 40 - 存储值:
在哈希表的索引40位置,存储"zs"和他的电话号码:
table[40] = ("zs", "1234567890")
结果:通过名字
"John",计算哈希值后找到索引40,即可快速检索到对应的电话号码。自行拓展
- 索引计算公式:通过哈希值和哈希表的 大小/长度 计算具体索引:
index = hashCode % tableSize。
-
确保每个索引 仅 对应一个桶
- 逻辑唯一性:对应哈希值的
一致性每个索引是唯一的,它对应唯一的桶,不会有两个索引指向同一个存储位置。 - 冲突管理
- 即使多个元素哈希值映射到同一个索引(即冲突),它们依然是存储在该索引唯一对应的桶中。
- 在桶中,冲突元素可以通过链表或红黑树的形式分开存储,但这些冲突元素仍然属于同一个索引。
- 冲突解决
- 一致性原则:
a.equals(b) == true,则a.hashCode() == b.hashCode()。
- 一致性原则:
意义
-
索引唯一性保证了哈希表结构的有序性和查找效率:
-
快速定位:通过索引直接找到对应桶,而无需全表扫描。
-
结构清晰:即使有冲突,冲突元素也明确归属于某个唯一的索引。
-
避免混乱:不同索引不会共享存储区域,确保了数据存储的安全性和分区逻辑的清晰性
-
数据存储与访问过程
- 存储过程:
- 添加元素时,
HashSet首先调用hashCode()方法计算哈希值,通过哈希函数确定元素在哈希表中的位置。 - 如果该位置没有其他元素,则直接存储;如果已有元素(哈希冲突),通过链表或红黑树解决冲突。
- 添加元素时,
- 访问过程:
- 查询元素时,同样调用
hashCode()方法计算哈希值,找到对应的存储位置。 - 如果该位置有多个元素(由于哈希冲突),则依次通过
equals()方法比较元素内容,找到目标元素。
- 查询元素时,同样调用
哈希冲突与存储优化
-
哈希冲突/挂载现象
不同元素的哈希值 可能哈希值相同 从而导致争抢 同一索引位置,称为哈希冲突。
- 例如:两个元素
A和B的hashCode()结果相同,它们会存储在同一个索引处,形成链表或红黑树(挂载现象)。
- 例如:两个元素
-
在 JDK8 中,为了对
HashSet在出现哈希冲突 时, 提高性能优化:-
红黑树优化存储:当发生哈希冲突时,传统链地址法(链表)会逐渐失效,尤其在链表过长时,性能会退化为线性查找 O(n)。为了解决这一问题,JDK8 引入了红黑树优化。
具体优化机制:
-
阈值条件:
如果某个桶的链表长度超过 8(默认值),则会将链表转换为红黑树。 -
转换过程:
-
当插入的元素导致链表长度达到阈值,哈希表会自动将冲突的链表节点转换为红黑树。
-
红黑树是一种自平衡二叉搜索树,可以将查找时间复杂度从 O(n) 降低到 O(log n)。
啥意思
快速锁定范围,就像查字典, 从首字母开头。
-
-
还原机制:
如果在后续操作中,红黑树中的节点数量减少到 6 以下,则会恢复为链表,减少空间开销。
优势
-
提升查找性能:
红黑树的 O(log n) 查找性能显著优于链表的 O(n)。 -
内存优化:
小规模冲突时,链表仍然是更节省内存的选择,而红黑树仅在必要时启用,兼顾性能和空间。
-
-
如果这篇文章帮到你, 帮忙点个关注呗, 点赞或收藏也行鸭 ~ (。•ᴗ-)✧

^ '(இ﹏இ`。)


2491

被折叠的 条评论
为什么被折叠?



