💡HashSet 元素位置
📕前言
我们都知道 HashSet 实现了 Set 接口, 并且是基于 HashMap 来实现的一个不允许有重复元素和元素无序(即不会记录插入的顺序)的集合。
通过这篇文章你可以了解到如何分析 HashSet 为新元素决定存储方式。
💡计算 hash 值
通过对象的 hashCode 计算出一个新的 hash 值
// 源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们不做 key 为空的情况的分析,因为 key 为空直接返回 0。
将上面的源码按执行步骤展开分析其 hash 值的形成。
int h;
h = key.hashCode();
int temp = h >>> 16;
h = h ^ temp;
key 我们知道是需要添加到集合里面去的元素,hashCode 方法是 key 提供的,如果为整数类型(Integer)的,那么 hashCode 方法就是返回数值;如果为字符串类型(String)的,那么 hashCode 方法就是返回通过 time31 算法计算出来的值;如果对象没有重写 hashCode 方法,那么就是使用 Object 提供的方法生成的值【详情请看】。
h = key.hashCode();
假设源码中 key = 123,
那么通过上面的描述 key 的 hashCode 方法返回的就是123,即 h = 123。
int temp = h >>> 16;
为了便于说明我创建了一个临时变量 temp 来记录无符号右移位运算的结果。
正整数右移 16 位可以简单地理解为小于 2 16 − 1 2^{16} -1 216−1(65535)的数其结果都为 0。
因此 temp = 0,因为 h 小于 65535。
h = h ^ temp;
最后将 h 与 temp 进行异或计算。
根据异或的特性:两个数相应位(二进制)的值不相同为 1,相同为 0 。
因为 temp 为 0,所以 h 的值就是异或的结果,即最后返回的 hash 值为 123。
💡计算元素在集合的存储位置
通过第一次计算的 hash 值与集合的长度计算出存储位置
// 源码 采摘与 HashMap 的 putVal 方法体里面的一段代码块
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这段代码就是决定元素在集合里面是直接索引赋值,还是放到现有的集合索引下通过结点连接在一起。
为了很好地讲解直接索引赋值,我们将上面源码步骤展开分析。
i = (n - 1) & hash;
p = tab[i];
if(p == null){
tab[i] = newNode(hash, key, value, null);
}
当然开始分析之前,先将参数都标记一下。
tab
代表当前集合(即 HashSet实例)
n
代表当前集合的容器个数
hash
代表添加到集合的元素的 hash 值
key
代表添加到集合的元素
value
代表 HashSet 的 PRESENT 属性
i
临时变量,用于记录索引
接着第一次生成 hash 值的步骤讲这个。
假设是第一次添加元素,元素为 123。
已知:hash = 123,key = 123,value = HashSet.PRESENT。
我们知道当集合第一次添加元素时,集合会创建 16 个空容器(即等于 null )用于存储元素,因此 n = 16。
i = (n - 1) & hash;
那么 n-1 与 hash 做按位与计算。
根据按位与的特性:两个数相应位(二进制)的值有 0 为 0 。
在这里 n - 1 的二进制前四位都为 1,因为 n - 1 等于 15。
从而得知 i 的结果就等于 123 的二进制的前四位,即 i = 11。
p = tab[i];
因此也就知道了 p 等于 null,可能你会疑惑为什么 p 等于 null?
因为当前集合 tab 有 16 个空容器,而 p 等于 tab 的第 12 个容器。
tab[i] = newNode(hash, key, value, null);
于是为当前集合的第12个容器赋值,newNode 方法会返回一个 Node实例。
💡总结
通过上面的描述,我们可知在 HashSet 集合里面添加元素,其位置是 hash 值与容器个数的与运算来决定的,而 hash 值是通过对象的hashcode 值右移16位与原 hashcode 进行异或运算得到的。
因此我们可以推导出元素存放在集合的第几个容器里面,例如元素 6 就是存放在集合的第7个容器里面,像 11 和 123 就会存放在集合的第12个容器里面(当然你的保证容器的长度为16)。
class Test{
public static void main(String[] args) {
Collection t = new HashSet();
int i = 0;
while (i++<122){
t.add(i);
}
t.add(123);
System.out.println(t);
}
}
你可以将断点打在 t.add(123) 这行上,通过进入源码可以发现元素 123 存放在集合的第 124 个容器里面,这是因为集合的扩容特性的存在,使容器个数不断变多。没有做 Object 的位置分析,但是可以通过重写 hashCode 方法来控制位置,意义不是很大。
其实里面通过控制 hashcode 去将元素挂载到容器里面去是蛮有意思的😀😀。
总结:元素存放在 HashSet 的位置由集合的容器个数与 hashcode 来决定的。
如果你是无意刷到这篇文章并看到这里,希望你给我的文章来一个赞赞👍👍。如果你不同意其中的内容或有什么问题都可以在下方评论区留下你的想法或疑惑,谢谢你的支持!!😀😀