JAVASE进阶:Collection高级(3)——HashSet、LinkedHashSet底层原理

‍作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
上期文章:JAVASE进阶:Collection高级(2)——源码剖析ArrayList、LinkedList、迭代器
订阅专栏:JAVASE进阶
希望文章对你们有所帮助

Set是Collection的另一个实现接口,与List相比,Set是无索引、元素不重复的,HashSet、LinkedHashSet以及TreeHashSet都是很常用的,在这里会弄懂一下HashSet以及LinkedHashSet的底层原理。
在这里就先不系统跟踪源码了,因为更值得跟踪的实际上是Map类的(HashMap、TreeMap等)。

HashSet、LinkedHashSet底层原理

  • 红黑树
    • 定义
    • 红黑规则
    • 添加结点规则
  • HashSet底层原理
    • 概述
    • JDK8以前的工作过程
    • JDK8以后的工作过程
    • 问题
  • LinkedHashSet底层原理

红黑树

在学习之前,简单看一下红黑树,我这里是一种复习,讲的不是很详细,要吃透红黑树需要自己先去系统学习一下。

定义

红黑树又叫平衡二叉B树,是一种特殊的二叉查找树,红黑树每个结点都有存储位表示结点的颜色。

每一个节点可以是红或者黑;红黑树不是高度平衡的,它的平衡是通过"红黑规则"进行实现的。

红黑树是二叉查找树,红黑规则不像平衡二叉树那样每次高度差超过1就旋转,那样会导致插入数据的效率变慢。

红黑规则

1、每一个结点要么红色要么黑色
2、根结点必须是黑色
3、若一个结点没有子结点或父节点,则该结点相应的指针属性值为Nil,这些Nil视为叶结点,每个叶结点(Nil)是黑色的
4、若某一个结点是红色的,则其子结点必须是黑色(即不能出现两个红结点相连的情况)
5、对每个结点,从该结点到其所有后代结点的简单路径上,均包含相同数目的黑色结点。

添加结点规则

1、添加结点时默认颜色为红色(效率会更高,如果是黑色的,会经常违背红黑规则的第5条规则)
2、添加根结点,直接变为黑色
3、添加非根结点,若父亲为黑色,则不需要任何操作
4、添加非根结点,若父亲为红色,则:
(1)叔叔为红色:
①将“父”设为黑,将“叔叔”设为黑
②将“祖父”设为红
③若祖父为根,再将根变回黑色
④若祖父非根,将祖父设置为当前结点再进行其他判断
(2)叔叔黑色,当前结点是父的右孩子,则把父作为当前结点并左旋,再进行判断
(3)叔叔黑色,当前结点是父的左孩子:
①将“父”设为黑色
②将“祖父”设为红色
③以“祖父”为支点进行右旋

HashSet底层原理

概述

HashSet有以下的特点:无序、不重复、无索引。
HashSet底层采用哈希表存储数据,JDK8以前,哈希表=数组+链表;JDK8开始,哈希表=数组+链表+红黑树。哈希表的核心是哈希值(对象的整数表现形式)。

哈希表最开始是一个长度为16,值全为null的数组,当要添加数据的时候,不是从0索引开始的,而是根据表达式
KaTeX parse error: Expected 'EOF', got '&' at position 24: …x = (数组长度 - 1) &̲ 哈希值
来确定数据存入的位置的。

哈希值

1、根据hashCode方法计算出来的int类型整数
2、该方法定义在Object类,所有对象都可以调用,默认使用地址值来进行计算
3、一般情况下,会冲洗的hashCode方法,利用对象内部的属性值来计算哈希值

对象的哈希值特点

1、若没有重写hashCode方法,则不同对象计算出的哈希值不同(因为默认使用的地址值肯定不相等)
2、若已经重写了hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
3、在小部分情况下,不同属性值或不同地址值算出来的哈希值是一样的,即哈希碰撞

想必大家都是学过哈希算法的,上面内容就不细说了。

JDK8以前的工作过程

JDK8以前,哈希表是数组+链表,工作过程如下:
1、创建一个默认长度为16、默认加载因子为0.75的数组,数组名为table
2、根据元素的哈希值与数组的长度计算出应存入的位置(index = (数组长度 - 1) & 哈希值)
3、判断当前位置是否为null,是则直接存入
4、若当前位置不为null,表示有元素,则调用equals方法比较属性值
5、equals方法判断是一样的,就不存储,如果不一样,就存入这个位置,形成链表

需要注意的是:

加载因子决定了哈希表的扩容时机,当数组中有16 * 0.75 = 12的空间被占用,数组会扩容成原来的2倍
JDK8以前,形成链表的方式是将新元素存入数组,老元素挂在新元素下面

JDK8以后的工作过程

JDK8开始,哈希表是数组+链表+红黑树

比起之前,大致的流程是一样的,但是细节上有些不一样:

JDK8以后,形成链表的方式是直接将新元素挂在老元素下面
数组长度 ≥ 64,且当前位置的链表长度 > 8时,当前的链表就会自动转换成红黑树,从而提高效率

因此,我们集合中存储自定义对象时,必须重写hashCode和equals方法。

问题

摸清原理就可以思考一些问题:
1、HashSet为什么存和取的顺序不一致?

HashSet的遍历是先遍历数组,每次到了数组的某个位置以后,把这个位置的链表或红黑树遍历过去,显然这种遍历出来的顺序不一定代表了存储的顺序,因为存储的方式是根据计算公式来的

2、HashSet为什么没有索引?

因为底层不纯粹,挂了红黑树或链表,不好再限定索引,因此舍弃索引

3、HashSet利用什么机制保证数据去重?

(1)用HashCode得到哈希值
(2)用哈希值确定当前元素应该被添加到数组中的哪个位置
(3)用equals比较这个位置的链表或红黑树中元素和新增元素是否相同,相同就不存入,从而实现去重

LinkedHashSet底层原理

LinkedHashSet有以下的特点:有序、不重复、无索引

LinkedHashSet其实底层数据结构还是哈希表,只是对于存入哈希表中的每个元素,都会按照存储的顺序去构建双链表,因此LinkedHashSet是有序的。

如果以后要数据去重,默认用HashSet,如果还要求有序,就需要LinkedHashSet。

你可能感兴趣的:(JAVASE进阶,java,jvm,源代码,Set,面试)