2021-10-19HashSet源码分析

关于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第一次添加元素和扩容小结

  1. 我们第一次添加元素首先会通过hash(key)算出hash值,这里的哈希值是通过HashMap的哈希算法计算出来的,不是直接使用的hashCode值

  2. 在添加之前我们会先判断数组(其他地方也有叫桶的,大家知道都是一个东西就行了)是否为空,如果为空的话则会先进行resize(),

  3. 这里的扩容会先对老容量是否大于0进行判断,如果大于0说明不是第一次初始化,再判断是否超过数组的最大值,没有的话,则会将老容量扩大二倍,并且将阈值扩大二倍

  4. 如果最大值为0,将默认容量16赋值给新容量,并计算得到新阈值

  5. 最后直接new一个容量为16的Node数组

  6. 接下来回通过哈希值与数组的长度-1进行&运算得到存放在数组中的位置,然后判断是否为空

  7. 没有直接添加,我们是第一次添加肯定没有元素,直接添加

    接下来我们继续讨论比较复杂的情况

第二次扩容

第一次添加元素后会对数据,数组也有了初始化的长度,我们可以正常的往里面添加数据,当我们添加完第十三个元素后,数组会进行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();
 }

文中的任何问题,或者疑问都欢迎大家在评论区指出,创作不易,大家动动小手帮忙点个赞。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值