HashSet底层实现解析

1.实现了Set接口
2.底层实际上是HashMap()
底层数据结构为:Node[]数组 + 单链表 + 红黑树
Set set = new HashSet();使用无参构造方法创建hashset对象,实际上创建了hashmap,初始大小默认为16,加载因子默认为 0.75.
在这里插入图片描述

当调用add方法添加元素时,其实调用的是map.put()方法
在这里插入图片描述

这里的key就是要添加的元素,value是一个object类型的共享常量,占了一个坑位,并没有实际意义。当返回值为null的时候,说明添加成功,解析:
首先,调用map.put();方法,跟踪发现实际上调用putVal();方法
在这里插入图片描述

参数说明:
hash(key):hash()函数的返回值,返回的并不是key的hashCode值,而是经过处理hashCode得到的值(目的就是尽可能减少hash冲突),(若待加入元素 key 为 基础数据类型 或 封装数据类型(Java中已经重写),不需要重写 equals 和 hashCode方法;若key为自定义数据类型,往往需要重写 equals 和 hashCode 方法。
例(不重写方法):
在这里插入图片描述

输出:
在这里插入图片描述

说明是两个不同的对象,但是理论上应该是相同对象。
具体要不要重写方法以及如何重写,都要根据实际需求决定

在这里插入图片描述

key:待添加的元素。
value:共享常量PRESENT,没有实际意义(因为创建的是hashset对象,并没有value,但是当顶层创建的是hashmap对象时候,调用的直接是map.put()方法,那value对应的就是实际当中的value,就没有PRESENT了)。
onlyIfAbsent*:若为true,则不改变现存的value,默认为false。
evict*:若为false,表示当前表处于创建模式。

putVal();方法的具体实现:

(1)首次添加元素 key
在这里插入图片描述

首先,创建两个临时变量 Node类型的数组 Node[] tab,Node<k,v>类型的 p,
判断当前hash表是否为空,若满足条件,调用resize()方法,扩容。
resize()方法:
在这里插入图片描述

由于用无参构造器创建对象,因此hashtable为空,oldCap为0,oldThr为0,if条件不满足跳过,进入else
在这里插入图片描述

新数组容量 newCap 为默认大小 16,新的加载因子为默认 0.75,newThr为扩容阈值(即当数组的数据量达到阈值以后,就会扩容,目的就是当并发量过大以后,能够有缓冲空间)。newThr = 加载因子*数组大小 ,在大小都默认的情况下,阈值为12
在这里插入图片描述

最后 return 扩容后的数组,并返回 putVal(); 方法中继续:
在这里插入图片描述

if ((p = tab[i = (n - 1) & hash]) == null) 中,首先用算法 i = (n - 1) & hash获得当前 待添加元素 key 应该添加的数组下标 i ,同时获取到当前位置上的所存储的链表的头节点,并将节点赋值给变量 p ,若该节点为空(即当前位置无数据),就把待添加元素 key 封装成一个节点Node,放到当前位置当作该链表的头节点。
在这里插入图片描述

最后操作计数+1,数据量size+1,并且若size大于扩容阈值,则进行扩容。返回 null 表示添加成功。(到此往数组中首次添加元素操作结束)
(2)添加重复元素流程分析
在链表上找是否有重复元素,只能从头节点遍历该链表。当 if 条件判断当前下标 i 位置上不为空的时候(即链表头节点),就要开始遍历。
在这里插入图片描述

定义需要的变量 Node<k,v> e (一个指针) ,以及 K k
首先判断头节点是否与待添加的元素重复:
1)hash值是否相等,这是前提(重复元素hashCode值必相等,则hash值也必相等)
2)待添加元素 key 与 当前节点的 key 是否是同一对象,或者带添加的 key 与当前节点的 key 内容 是否相等,两者若有一个相同则说明是重复元素

若头节点就重复,则把头节点 p 赋给 e;(否则,判断当前头节点 p 是否是红黑树类型)
在这里插入图片描述

若是树类型,则按照树节点进行操作。
若不是树类型,则继续遍历当前链表:
在这里插入图片描述

这里的 for 是一个死循环,退出的条件就是找到重复元素或遍历到链表结尾(肯定可以退出)。判断是否重复,与上面的条件一样。
①若遍历到结尾没有发现重复元素,则将待添加的元素 key 封装成Node对象,添加在链表结尾,同时,检查当前链表节点数量是否达到树化(将链表转成红黑树)的阈值默认是 8,若达到阈值,则首先检查当前数组的大小是否达到64,若达到,则将该链表转成红黑树;若没达到64,则先对数组进行扩容;否则退出循环,返回 e 为空。
static final int TREEIFY_THRESHOLD = 8;
②若遍历发现有重复元素,退出循环,此时的变量 e 就是重复元素。
若顶层创建的对象是HashSet();则下面的语句没有意义,因为value值是共享的PRESENT;
若是顶层是 HashMap(k,v) 对象,下面语句表达的是:当在map数组中发现已经存入key了,就把待添加键值对的value 替换掉旧的value,key不变。

在这里插入图片描述

不管创建哪种对象,返回值都不为空,说明添加失败。(到此添加重复元素流程结束)
补充总结:
①hashSet()底层实现的其实是hashMap(),数据结构相同都是数组+单链表,元素数据类型是HashMap的内部类Node类型–Node<k,v>
②利用无参构造方法创建HashSet,默认大小16,加载因子 0.75,扩容为原大小的 2 倍,扩容阈值 = 数组大小 * 加载因子。(也可用有参构造方法创建对象,设定大小或加载因子)
③HashSet中的add();方法其实就是map.put()方法,只是由于添加的都是单一元素(参数key),并不是<k,v>键值对,所以统一用共享value(常量PRESENT)。
④HashSet不允许存储元素重复,就和HashMap不允许重复的key一样,只是HahsSet没有用到value。
⑤添加元素的时候,要先得到key的hashCode值,经过hash函数后得到hash值,再经过一次计算得到待添加的key应该存放的数组下标。若当前下标对应位置不为空,则遍历该链表,查找是否有重复元素(若重复,则不添加;反之,加在链表尾部);若当前下标位置上为空,则之间添加到当前位置并作为头节点。(这也就是为什么,实际存储顺序和实际输出顺序不一致)

内容和细节还有不足之处,希望大家指正!!

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值