HashMap、HashSet源码分析

11 篇文章 0 订阅

1、基本结论

1.1、底层实现

HashSet是由HashMap实现的,而HashMap是一个个键值对的集合,因此HashSet中的值作为HashMap的key,而自动new一个Object对象最为HashMap的value,这个Object的引用是静态常量的引用类型,所以整个内存仅存在一个Object,意味着所有的Set对应于HashMap中的value都是这个Object。因此,分析HashSet的源码实际就是分析HashMap的源码。

1.2、基本机制

  • 对于HashMap,存在一个容量值和一个扩容阈值,当Map中的元素数量达到了阈值,则进行相应的扩容。而初始容量值是16,初始的阈值是16*0.75=12.
  • 之后在加入元素到达阈值后,要进行扩容,新的容量是旧容量的两倍,新的阈值是旧的阈值的两倍。如 旧容量是16,新容量就是32,旧阈值是12,新的阈值是24,同时,新的容量和阈值之间也满足 阈值=容量*0.75
  • HashMap是基于哈希表的,而次哈希表的冲突处理是采用链表法进行解决的。即,如果待加入的元素的哈希值对应哈希表中的位置已经存在一个元素了,并且这个元素和待加入的元素不相等,则把这个元素链接到冲突位置元素的链表的表尾。
    如:k3和k1冲突了,此时把k3链接到k1后,k5以此类推。在这里插入图片描述
  • HashMap的哈希表用一个数组表示,而这个数组的类型是Node<k,v> [],Node<k,v>继承子Map的静态内部类Entry<k,v>,实际通过HashMap得到的keySet就是HashMap里存储的Node<k,v>,keySet的只不过是该Node<k,v>的引用。
  • 当HashMap的哈希表的数组中的某一个位置上的链表长度达到了8,并且整个数组长度达到了64,则会把这个链表转化成红黑树;某一个位置上的链表长度达到了8,而整个数组长度未到64,则会先翻倍这个数组的长度
  • 假如重复加入重复元素,对于Set来说,由于是把要加入的值当Map中的key存储,因此如果加入完全相同的key,则会用充当value的Object 对原来的Object进行覆盖,相当于自己覆盖自己。对于HashMap,则会用新的value去覆盖旧的value。

2、源码分析

2.1、插入元素

首先看HashSet,写下面代码,加入一个3

		Set<Integer> set = new HashSet<>();
        set.add(3);

单步调试进入首先对3进行自动装箱:
在这里插入图片描述
其中,IntergerCache.low=-127,而IntegerCache.high = 128,cache是一个容量为256的Integer型的数组,这里会把-127到128之间的数通过new Integer(i)的方式一个个缓存在cache数组中,即如果这个基本数据类型在-127到128之间,则直接返回数组中存的Integer对象,否则new 一个Integer返回。源码如下:
在这里插入图片描述
装箱结束之后,会进入HashSet的set方法:
在这里插入图片描述
可以看到这个set方法实际调用了map中的put方法,而这个map就是HashMap,把要加入的值当做HashMap中的key,而value则使用一个静态常量PRESENT代替。
在这里插入图片描述
接下来的分析实际就是分析HashMap的put源码了:
在这里插入图片描述
首先会通过这个key计算它的哈希值:在这里插入图片描述
可以看到如果这个key是空的话,哈希值就会是0,也即意味着如果加入的key是null类型的话,会放在0
号位置。也可以看出无论HashSet还是HashMap,都是可以把key和value存放null的。
随后进入putVal方法:
在这里插入图片描述
可以看到注释中对于onlyIfAbsent的解释,如果这个值是true,则当key相同时,不会去覆盖相应位置的元素,从上一张图中可以看到,这个值传入的是false,意味着在key相同时,是通过覆盖的方式去处理的。这也是HashMap更新value的处理方式。
在这里插入图片描述

  • 首先,如果table为null,即第一次添加元素,则会调用resize()函数进行初始化,后面再分析resize();
  • 其次,如果key对应hash值的table表上位置不存在元素,则直接new一个Node插入即可
  • 如果key对应hash值的table表上位置存在元素,则发生了hash冲突,此时对应三种情况:
  1. 如果冲突的元素的key和待插入的key完全一样,则会把原始的node(即 p)赋值给e,在下面的位置对该Node的value’进行覆盖替换,这也是HashMap更新对应key的方式:
    在这里插入图片描述
  2. 如果这个链表已经转化成了树,则调用树的处理方法
  3. 以上两种情况都不符合,则需要对这个位置对应的Node链表进行迭代对比其中的key是否相同,如果发现已经存在了一个一样的key,则跳出for循环,通过上图的方法对该位置的Node中的value进行替换。 如果对比到链表的末尾还没有key和他一样,则插入到表尾。插入后,对链表长度进行判断,如果长度到达了TREEIFY_THRESHOLD - 1,即8,则调用treeifyBin()方法把链表转化成红黑树。进入这个方法:
  4. 在这里插入图片描述
    各个阈值声明如下:
    在这里插入图片描述
    可以看出,如果链表长度到达了8,而table数组没有到64时,会重新扩容而不进行树化的。
    putVal()方法的最后:
    在这里插入图片描述
    判断加入这个元素后是否到达了阈值,到达了就进行扩容,否则返回null。这里这个null对于HashSet来说,是用来判断是否插入成功的,如果插入一个已经存在的key,则会返回原来这个值,从而插入不成功。对于HashMap来说,没有影响,会返回替换的值。

2.2 resize()源码分析

在这里插入图片描述

在这里插入图片描述
具体扩容步骤都在图中标出,这里补充一个指定容量的方式:

 Set<Integer> set = new HashSet<>(3);

如果对HashSet指定容量,则会调动HashMap的构造方法构造一个map:
在这里插入图片描述
实际还是对HashMap进行分析:
在这里插入图片描述
在这里插入图片描述
随后在进行resize()的时候就会对应相应的if条件进行容量的初始化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值