你真的了解HashSet集合么?

概述:你真的了解HashSet集合么?用了怎么久的集合你真的了解集合么?

**注意:该文章是基于JDK8讲解的**

那不妨先看几个问题,如果你都能知道那么就说明你真的了解,如果感觉似曾相识,但是又说不上来,那么就是认真的往下看吧?

想要看懂集合源码需要具备哪些知识?
数据结构和算法,javaSE的面向对象,进制转换,以及java的一些运算符,比如位移,异或等等。

理解源码的方式就是打断点一步一步的跟踪,看源代码是如何执行的,遇到看不懂的代码就多看几次。

这个文章是我在没有学习数据结构和算法的前提下理解的,所以呢?也行解释的不够透彻,请多多谅解,后期肯定会发布关于数据结构和算法的一些文章。

1.带着问题学习?

基础部分问题?

1.HashSet底层是什么数据结构?

2.HashSet允许有空值么?

3.HashSet允许有重复值么?

4.如果new两个值一样的字符串,往HashSet集合中添加,是否能添加进去?

5.HashSet是如何保证元素的唯一性的?

6.HashSetadd方法其实是调用的那个方法?

7.HashSet是否是线程安全的呢?

上面的几乎都没有什么难度,使用过集合的大多数人都了解。

那我们来看看哪些硬核的难点吧!

我们都知道HashSet底层其实上是new了一个HashMap集合,那我们就来看看,HashSet调用add方法的时候的一些问题。

1.HashMap的value部分值是否相同?

2.HashMap的初始化容量是多大?是在什么时候进行初化容量?

3.在计算HashMap的key的HashCode值的时候是单纯的时候hashCode方法计算出来的么?

4.HashMap什么时候进行扩容?

5.HashMap数组转红黑树需要满足那些条件?

7.HashSet在添加重复元素的时候,具体是怎么进行判断该元素已经存在的?

8.使用HashSet集合的时候,需要重写HashCode和equlas方法么?

2.问题答案:

基础部分问题?

1.HashSet底层是什么数据结构?

答案:HashSet底层采用的是数组加链表加红黑树,在new HashSet的时候实际底层是new了一个HashMap,把HashMap的key部分,作为HashSet的Value部分。

2.HashSet允许有空值么?

答案:准确的来说是允许的(也就是代码不会出现异常),但是只能有一个空值,如果有第二个空值,那么第二个空值将加不进HashSet集合。

3.HashSet允许有重复值么?

答案:肯定是不允许的,因为HashSet的value部分是HashMap的key部分,因为HashMap的key本身就是无序不可重复的,所以HashSet也就不可能重复。

4.如果new两个值一样的字符串,往HashSet集合中添加,是否能添加进去?

答案:是不可以加入进去的,因为在进行添加元素的时候会进行判断,通过hashCode方法和equals方法进行比对,String这个类,重写了这两个方法,比较的是字符串的值,而不是使用继承自Object的equlash和hashCode方法去进行比较。

5.HashSet是如何保证元素的唯一性的?

答案:依赖于hashCode()和equals()这两个方法,所有在我们比较两个我们自定义的对象的时候,需要我们重写这两个方法,自定义比较规则,否则就是使用继承自Object的进行比对,比对的是对象的内存地址。

6.HashSetadd方法其实是调用的那个方法?

答案:其实调用的是HashMap的map.put方法。

7.HashSet是否是线程安全的呢?

答案:HashSet是线程不安全的,所以呢?他的执行效率比较高,因为HashSet和HashMap的源代码中的方法都有没有加synchronized关键字。

那我们来看看哪些硬核的难点吧!

1.HashMap的value部分值是否相同?

答案:都是相同的,因为value部分是使用了一个静态的Object对象进行占位,这个对象只是用于占位操作,并没有多大的实际意义。

2.HashMap的初始化容量是多大?是在什么时候进行初化容量?

答案:初始化容量是16,是在第一次调用resize()方法的时候进行扩容的,并不是new HashMap方法的时候就进行扩容。

3.在计算HashMap的key的HashCode值的时候是单纯的时候hashCode方法计算出来的么?

答案: 不是,而是通过一个表达式进行计算后的结果((key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)),并不是单纯的hashCode值。

4.HashMap什么时候进行扩容?

答案:底层数组超过临界值12的时候就会进行扩容,那么为什么不是到16才进行扩容呢?试下一下,他是一个线程不按的集合,万一此时突突来了很多对象,要加入到这个集合,那么这个集合不就炸了么?扩容的机制就是:当前数组容量乘以2再乘以加载因子0.75

也就是说,只要这个集合中添加了12个元素后,底层数组就会扩容到32,因为每次添加元素的时候都会++Size,并不是说,这个数组中满了12个单向链表的时候进行扩容。

扩容判断的核心源代码:

        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);

5.HashMap数组转红黑树需要满足那些条件?

答案:首先判断该链表是否已经到达8个节点,如果满足该条件,再次进行判断这个数组链表的值是否大于64,如果小于64,还不会转化为红黑树,而是进行数组的扩容,大于64再转红黑树。

7.HashSet在添加重复元素的时候,具体是怎么进行判断该元素已经存在的?

答案:进行equlas方法和HashCode方法进行比对,如果比对不出来再进行判断该链表是不是一颗红黑树,是的话进行红黑树的方式进行判断,如果不是,那么就遍历该链表,依次进行比对,如果比对到匹配的值,那么添加失败,如果没有比对到相等的值,那就把该元素添加到该链表的末尾。

8.使用HashSet集合的时候,为什么要重写HashCode和equlas方法?

答案:因为底层添加元素的时候会调用这两个方法进行比对,而这个两个方法就是需要我们自定义比对规则,不然默认继承Object的。

3.源码分析,证明答案

首先看下HashSet的继承结构图:
在这里插入图片描述

1.Iterable接口: 顶级父接口,只要实现这个父接口,那就说明该集合是可遍历的,而且有迭代器对象。

2.Collection接口:说明实现这个接口的集合都是单value的形式存在。

3.AbstractCollection抽象类:这个抽象类实现了一些基本的方法,比如toArray()还有toString()等等,定义了一些抽象方法等待子类去实现。

4.Set接口:说明实现该接口的集合都是无序不可重复的。

5.Serializable接口:实现这个接口,就标志着该类是支持可序列化的,也就是把对象保存的磁盘。

6.Cloneable接口:一看这个单词就知道,实现该接口的类都是支持可克隆的。

7.AbstractSet抽象类: 该抽象了实现了equals方法和hashCode,removeAll方法。

了解完HashSet的基本机构图后,我们对HashSet具备的功能就有了一定的了解。

那么我们简单的来看一下数组+链表+红黑树的这这种数据接口的特点:

数组:数组的特点是有序的,查询比较快,但是呢?随机增删就比较慢了,因为随机增删元素涉及到元素的位移操作。
在这里插入图片描述

链表:链表是随机增删比较快,但是呢?查询比较慢,因为查询的时候会从头节点开始查找。

来,有图有真相:
在这里插入图片描述
红黑树:留到下次将HashMap的时候再讲,因为这里主要是介绍hashSet。因为红黑树极其复杂,不是一两句话可以说明白的。

在这里插入图片描述

我们知道,程序其实就是数据结构加算法组成的,所以,程序的精妙之处就是数据结构和算法,不同的业务可能选择不太的数据结构去存储数据,既然他们各有各的优点,那么就可以尝试着把他们组合起来,让他们发挥各自的特点。

在没有树化之前的数据结构图:大概是这样的:

在这里插入图片描述

new HashSet的源码:


       //执行构造器
         public HashSet() {
                map = new HashMap<>();
           }

1.第一次调用add方法的源码分析:

        //  第一次add方法的执行过程:
         //   2.add方法 :调用map的put方法 PRESENT:静态的一个Object对象 用于占位,每一个map的value都是用一个对象
         *         public boolean add(E e) {
         *         return map.put(e, PRESENT)==null; //如果return null的时候就代表执行成功了
         *     }
         *     // 调用hash方法获取到key的hash值
         *     3.  public V put(K key, V value) {
         *         return putVal(hash(key), key, value, false, true);
         *     }
         *     // 通过hash算法获取的key的hash值 此hash值并不等于key原本的hash值
         *         static final int hash(Object key) {
         *         int h;
         *         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
         *     }
         *
         *     4.得出hash值后 然后去putValue方法判断是否应该把这个值添加进去
         *      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
         *                    boolean evict) {
         *         Node<K,V>[] tab; Node<K,V> p; int n, i; //定义辅助变量,建议我们在开发的时候,需要用的时候再进行定义临时变量
         *         // 第一次进来 if成立,调用resize()
         *         if ((tab = table) == null || (n = tab.length) == 0) //table 其实就是HashMap里面的那个Node数组[] 存放链表的那个数组
         *             n = (tab = resize()).length;  //resize())执行完后,返回一个初始化容量为16的table[]数组
         *
         *             // 通过key的hash值计算出元素应该存放到table数组的那个索引位置
         *             //并把这个位置的对象赋值给临时变量p,判断p是否为null
         *             //如果p为空,代表这个位置还没有存放过元素,就创建一个node对象,key和value都放进去,next为null,留给第后来添加的元素存放Node对象
         *         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;
         *             }
         *         }
         *         // 记录修改的次数
         *         ++modCount;
         *         if (++size > threshold) //判断当前这个table数组是否超过了12这个最大容量值,如果超过进行扩容
         *             resize();
         *             // 这个方法其实是一个空方法,是留给子类去实现的
         *         afterNodeInsertion(evict);
         *         return null; //程序走到这儿,就代表我们第一次添加的元素已经成功了
         *     }
         *
         *     5. resize()方法: 初始化数组链表的初始容量10,并且判断该数组是否要进行扩容
         *      final Node<K,V>[] resize() {
         *         Node<K,V>[] oldTab = table; //此时的table=0
         *         int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap=0
         *         int oldThr = threshold;
         *         int newCap, newThr = 0;
         *         if (oldCap > 0) { //table不大于0 false
         *             if (oldCap >= MAXIMUM_CAPACITY) {
         *                 threshold = Integer.MAX_VALUE;
         *                 return oldTab;
         *             }
         *             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
         *                      oldCap >= DEFAULT_INITIAL_CAPACITY)
         *                 newThr = oldThr << 1; // double threshold
         *         }
         *         else if (oldThr > 0) //  oldThr=0不成立
         *             newCap = oldThr;
         *         else {               //
         *             newCap = DEFAULT_INITIAL_CAPACITY; //初始化HashMap的容量为16
         *             //最大容量值为 0.75X16=12,当数组中的元素超过12个的时候,数组链表开始扩容
         *             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
         *         }
         *         if (newThr == 0) {
         *             float ft = (float)newCap * loadFactor;
         *             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
         *                       (int)ft : Integer.MAX_VALUE);
         *         }
         *         threshold = newThr; // 最大容量值
         *        // 创建一个初始化容量为16的table[]
         *         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;
         *                         }
         *                     }
         *                 }
         *             }
         *         }
         *         最终返回一个 初始化容量为16的table[]数组
         *         return newTab;
         *     }
         *

2.第二次调用add方法的源码分析


         *    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是不等于null的
         *         if ((tab = table) == null || (n = tab.length) == 0)
         *             n = (tab = resize()).length;
         *          // 通过计算后的key的hash值,算出元素存放在数组中的那个位置
         *         if ((p = tab[i = (n - 1) & hash]) == null)
         *         // 创建一个 Node对象放到Node数组中(table[])
         *             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;
         *             }
         *         }
         *         // 记录修改次数
         *         ++modCount;
         *         // 此时为2 不大于12 所以不进行扩容
         *         if (++size > threshold)
         *             resize();
         *         afterNodeInsertion(evict);
         *         //返回null说明添加成功
         *         return null;
         *     }

3.x向集合中添加相同元素的分析:


         *
         *    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
         *                    boolean evict) {
         *         Node<K,V>[] tab; Node<K,V> p; int n, i;
         *         //只有第一次add的时候才会执行这个 if
         *         if ((tab = table) == null || (n = tab.length) == 0)
         *             n = (tab = resize()).length;
         *             // 此时这个 方法为false 因为这次添加的元素是我们上次已经添加过的元素,所以算出来的下标1肯定是和上一次算出的下标一致
         *             // 判断这个数组的下标位置中是否已经有链表元素存在
         *         if ((p = tab[i = (n - 1) & hash]) == null)
         *             tab[i] = newNode(hash, key, value, null);
         *         else {
         *         //添加重复值的时候执行:
         *             Node<K,V> e; K k;
         *             // 此时的这个p就是指向的上面算出来的数组下标里的那个Node对象
         *             //如果当前索引位置对应的链表的第一个元素和准备添加的这个key的hash值hash值相同
         *             if (p.hash == hash &&
         *             //如果hash值相同的情况下 当前准备要加入的key和刚刚计算出来的数组下标对应的那个Node对象的key是同一个对象 或者
         *             // 当前的这个key不为null然后在和计算出来的那个数组下标对应的那个Node对象里的key进行equals比较,
         *             //如果没有重写那么调用的就是继承自Object的equals方法,如果重写过,那么就调用重写后的,hashcode方法也是一样,所以建议两个方法都重写
         *
         *                 ((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 {
         *             // 假如不是红黑树,那就是第三种情况:按照上面第一个情况的方式依次和整个链表进行比较,如果找到条件满足的那就直接break(此元素已经存在);
         *             // 结束遍历,return oldValue 那么就代表着添加失败,如果说,比较完后都没有满足条件的(该元素不存在),那就挂载到这个链表的末尾
         *
         *             // 在把元素添加到最后,立即判断 该链表是否已经到达8个节点,如果到达,调用treeifyBin(tab, hash);方法把当前这个链表转化为红黑树
         *             判断条件如下:   if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)   resize();
         *     // 上面条件不成立才进行树化 再进行转红黑树时还进行判断这个数组链表的值是否大于64,如果小于64,还不会转化为红黑树,而是进行数组的扩容,大于64再转红黑树
         *                 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;
         *                     }
         *                     // 如果 比的过程中找到一个值与准备添加的元素的值一致,那么就直接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;
         *             }
         *         }
         *         ++modCount;
         *         if (++size > threshold)
         *             resize();
         *         afterNodeInsertion(evict);
         *         return null;
         *
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值