关于HashSet底层的源码研究分析
HashSet是我们经常用到的一种集合,它有着无序,不能重复的特点,只能有一个null值,接下来我会通过代码测试演示为什么是这样的
其实HashSet底层不是什么特殊的数据结构,它就是HashMap的key。接下来让我们先看一下测试代码,
public static void main(String[] args) {
Set hashSet = new HashSet();
for (int i = 0; i < 12; i++) {
hashSet.add(i);
}
}
}
我会通过断点对代码进行测试并解释整个过程
//这是HashSet的无参构造,很容易发现底层就是直接new了一个HashMap
public HashSet() {
//map是HashSet定义好的一个HashMap初始值为null,HashSet的数据都是存于这个map集合中
map = new HashMap<>();
}
可能有了解的小伙伴会有这样的疑问,HashSet是单列集合,而HashMap是双列集合,HashSet就一个数据怎么存进去的?
HashSet存放数据会将存放的值作为Key和一个常量PRESENT作为value一起存入HashMap中
我们接着看他的添加过程
HashSet的add方法
//这是HashSet的添加方法,直接就调用了HashMap的put方法,所以我们就直接看put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//这里的真正添加元素的方法是putVal(),这里的hash(key)是计算哈希值的,也就是说HashMap的添加使用了自己的哈希算法,没有直接使 //用hashCode()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//接下来我们继续看putVal()方法是怎么添加元素的?
HashMap的putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//首先定义一个Node数组长度为0,第一次初始化后长度会变为16,使用的resize()方法进行扩容,和定义一个Node节点
//这个节点就是用来存放即将添加的数据的
Node<K,V>[] tab; Node<K,V> p; int n, i;
//首先对数组进行非空判断,如果为空的话就要进行初始化扩容通过resize()方法,我把resize方法单独拉出来放在这段代码的下面
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过hash值得与运算得到数据存在的位置,判断当前位置是否有节点
//我们是第一次添加肯定不会有元素,所以不会重复直接就添加完成了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//将修改次数+1
++modCount;
//将大小+1并判断是否需要扩容
if (++size > threshold)
resize();
//HashMap提供的空方法,子类可以实现其功能
afterNodeInsertion(evict);
//返回空值
return null;
}
令人瞩目的resize()方法
final Node<K,V>[] resize() {
//变量太多了,我就用中文意思去说,也省的大家搞混了,大佬们的命名都比较规范,很容易看出来意思
//将现在存在的数组赋值给老数组(oldTab:null)
Node<K,V>[] oldTab = table;
//得到老数组的容量为老容量(oldCap:0),看不懂的可以去百度一下三元运算
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//得到老的阈值为老阈值(oldThr:0),这个阈值是数组是否扩容的关键,
int oldThr = threshold;
//创建两个变量分别为新容量(newCap:0)和新阈值(newThr:0)
int newCap, newThr = 0;
//--------------------------------------------
//横线里面的内容都是为了确定新阈值(newnTHr)和新容量(newCap)
//当老容量(oldCap)大于0时,在判断是否超过最大的数组容量
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//当老容量(oldCap)扩大二倍后成为新容量(newCap),
//当新容量(newCap)小于数组的最大容量并且老容量大于默认容量(16)
//这句话的意思就是数组已经不是第一扩容了,
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//将新阈值(newTHr)扩大为二倍
//这一点很好的理解,阈值等于最大容量乘以负载因子,当最大容量扩大二倍,负载因子不变的话,阈值也扩大二倍
newThr = oldThr << 1; // double threshold
}
//当数组不是空数组时
else if (oldThr > 0) // initial capacity was placed in threshold 初始容量被置于阈值
//待敲定
newCap = oldThr;
//初始化阈值被使用,也就是说数组还是空数组
else { // zero initial threshold signifies using defaults
//使用新容量值为默认容量(16)
newCap = DEFAULT_INITIAL_CAPACITY;
//计算的到新阈值(12)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//判断新阈值是否0,这个判断存在的意义就是为了防止程序走了这个 else if (oldThr > 0),而没有设置新阈值
if (newThr == 0) {
//设置新阈值的值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//----------------------------------------------
@SuppressWarnings({"rawtypes","unchecked"})
//将数组进行初始化
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//==================
//等号里面的内容这一次没执行,因为我们是第一次进行扩容,所以老容量为空
//我们先继续往下走,等下我会回过头再填这个坑
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//=======================
//到这里我们就得到了初始化后的数组,再回到上面的添加过程
return newTab;
}
HashSet第一次添加元素和扩容小结
-
我们第一次添加元素首先会通过hash(key)算出hash值,这里的哈希值是通过HashMap的哈希算法计算出来的,不是直接使用的hashCode值
-
在添加之前我们会先判断数组(其他地方也有叫桶的,大家知道都是一个东西就行了)是否为空,如果为空的话则会先进行resize(),
-
这里的扩容会先对老容量是否大于0进行判断,如果大于0说明不是第一次初始化,再判断是否超过数组的最大值,没有的话,则会将老容量扩大二倍,并且将阈值扩大二倍
-
如果最大值为0,将默认容量16赋值给新容量,并计算得到新阈值
-
最后直接new一个容量为16的Node数组
-
接下来回通过哈希值与数组的长度-1进行&运算得到存放在数组中的位置,然后判断是否为空
-
没有直接添加,我们是第一次添加肯定没有元素,直接添加
接下来我们继续讨论比较复杂的情况
第二次扩容
第一次添加元素后会对数据,数组也有了初始化的长度,我们可以正常的往里面添加数据,当我们添加完第十三个元素后,数组会进行resize(),这里进行扩容的判断条件如下:
//当添加完数据后size的大小大于阈值才会进行扩容
if (++size > threshold)
resize();
上面没填的坑,在这里填
// Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//经过上面的扩容 ,已经成功得到了这样一个NOde数组,那么下面的步骤为啥还存在?
//我们都知道数组长度是不能变换的,宏观上的数组长度的变化在底层是通过,创建一个新数组,并且将原来数组中存在的值进行拷贝
//所以我们这一步的存在意义就体现出来了,就是将原来已经存在的数据复制到新的数组中
//这个就是判断数组中是否存在的入口
if (oldTab != null) {
//通过索引对数组进行遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//首先判断当前索引出是否存在元素,有元素就继续,没有元素就跳到下一次循环。
if ((e = oldTab[j]) != null) {
//将索引处的位置变为空,这样是为了当垃圾回收器可以将其回收,真正的元素已经被赋值给e
oldTab[j] = null;
//先判断是否为一个元素,
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//判断是否为树节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//链表长度大于1且不为还没进行树化
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
最后说一下HashMap树化和扩容的条件
树化
HashMap树化的条件一定要满足两个条件。
链表长度大于等于8,并且数组长度大于等于64
我们可以看一下,我用截图的形式给大家展示
从下面的箭头中我们可以看出当前的容量为7,数组长度为16,如果我们再添加一个元素,有很多人会认为他就会进行树化,其实不然,链表会先进行扩容,我们继续看下一个图片
当size=8时,size大小在下面截图截不到了,我们很容易出来,此时还是Node节点还未进行树化,但是数组已经进行了扩容
也就是每次进行树化之前都会对数组长度进行判断,如果数组长度为0,或者小于最小树化容量(64)j就会就会进行resize()
代码永远比口述具有说服力
//这是添加元素后是否树化的判断
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//这是树化的方法,第一步就是先对数组长度进行判断
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
}
文中的任何问题,或者疑问都欢迎大家在评论区指出,创作不易,大家动动小手帮忙点个赞。