HashMap源码分析

本文详细介绍了HashMap的数据结构,包括数组和链表(或树)的组合,以及如何通过(n-1)&hash算法定位元素。在元素增删改查时,HashMap会根据链表长度转换为红黑树以优化查询效率。当元素数量超过阈值时,HashMap会进行扩容,扩容过程中巧妙地保持元素位置关系。此外,文章还阐述了get函数的查找过程,以及链表到红黑树的转换策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本原理

HashMap的数据结构是由数组和链表(或者树)组成。当进行元素的增删改查时,首先要定位到元素所在桶的位置,也就是table数组的下标,之后再从链表或树中定位该元素。定位下标使用 (n - 1) & hash 算法,其中n为数组长度,hash为key的哈希值

hashmap是通过一个数组Node<K,V>[] table(该变量是一个类变量)存储所有的数据,每个元素存放元素的下标为putVal()函数中对应的局部变量p,其计算方法为:

p = tab[i = (n - 1) & hash]

其中tab即为table,n为table的长度,hash为key的哈希值

img

存放数据的数组中每项的数据结构是Node或者TreeNode.当数组某项对应的长度小于8时,默认使用单向链表来存储元素,当数组某项对应的长度大于等于8时,此时会将链表转化为红黑树.同理,当TreeNode中包括的元素数量较小时,此时会将红黑树转化为链表形式存储.

此处为什么需要这么做?

主要是基于查询的效率考虑.链表结构查询元素的时间复杂度为O(n),随着链表长度的增大,查询时间也会递增.而红黑树的时间复杂度为O(logn),红黑树虽然占用空间更大,此处相当于以空间换时间.

HashMap中几个特殊值说明:

  • initialCapacity HashMap初始容量, 默认为16
  • loadFactor 负载因子,默认0.75
  • threshold 键值对数量的最大值,超过这个值,则需要扩容,该值在进行扩容的时候会改变为原先值的两倍
插入流程

首先计算key的哈希值, 然后调用putVal()函数。

下图是putval()函数的源码, 就其中的关键几步进行说明

image-20210323152030654

  1. 当存储数据的数组为空,则调用resize()函数进行数组的初始化操作。该处代码表明HashMap中数组的初始化并不是在new的时候,而是在第一次put的时候。

  2. 使用(n - 1) & hash计算数组下标,若当前项不存在,则调用newNode()函数在数组下标处新建一个节点。

  3. 当以上条件都不满足, 说明该处索引处已经存在节点,则需要判断该节点处的key和待存入数据的key是否相等。3、4、5是对应不同类型的。其中3是直接判断当前节点, 4则是树节点(不需要判断), 5是链表。 判断相等的条件如下(之间是且的关系):

    • 将要存入数据key的哈希值和数组下标处节点的哈希值相等
    • 将要存入数据的key和数组下标处节点的key相等(此处相等是内存地址相等或者equals比较相等都可以)
  4. 该代码表明该处数组下标处的节点是树节点, 所以此时调用putTreeVal()函数插入树节点。

  5. 当3和4都不满足时,则表明该处是一个链表,则进行链表的遍历。

  6. 若一直能遍历到链表尾部,则在链表尾部新建一个节点储存当前待存入数据。然后判断是否要将链表转化为红黑树, 若是则调用treeifyBin()函数进行链表的树化。

  7. 若链表中某个节点的key与待存入数据的key相等(与第3步的判断条件一样),则退出遍历。

  8. 判断之前是否已经存在key值相同的节点,若是此时根据onlyIfAbsent参数来决定是否将之前节点的值更新为本次要插入的值(实际上map默认的put操作该值为true,表明是覆盖插入)

  9. 判断是否要进行扩容操作, 若是则调用resize()函数进行扩容。注意此处的++resize操作可能导致线程不安全。

扩容

resize()函数。其用途用来进行扩容或者初始化数组。

下面是resize函数关键几步分析:

image-20210324180901686

基本流程:

  1. 当扩容之前的数组长度大于最大值时,直接返回未扩容之前数组(也就是不进行扩容),此处表明当hashmap容量大于最大值时(Integer.MAX_VALUE),就无法继续插入新值
  2. 当元素个数大于阈值(默认初始容量16 * 负载因子0.75 = 12)时进行扩容,将新数组长度和阈值都扩大为原先2倍
  3. 能进入该分支表明是第一次初始化,设置数组容量大小(指定值或者默认值)和阈值大小
  4. 重新初始化一个新的数组,数组长度是原先的2倍
  5. 开始进行重哈希,也就是将原数组项的数据重新放入新的数组项里面,此处需要遍历原数组
  6. 此处表明当前数组项只有一个节点(肯定是链表节点),则直接将该项值插入到新数组项中
  7. 此处表明当前数组项是一个红黑树,进行红黑树的重新赋值
  8. 当以上两种情况都不符合,肯定是链表节点,则进行链表的重哈希,需要遍历链表

链表节点的重哈希 有必要说明一下,此处设计的巧妙,具体描述可见文章:https://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

JDK中解释resize函数的说明大意如下:

当进行扩容时,因为我们每次扩容容量都是之前的2倍,任意元素在新数组中要么是原先的位置,要么是原位置移动2次幂的位置。

其具体原理是利用0与0或者1进行&运算结果都是0,这样在进行重hash的时候,不需要重新计算Hash值,只需要判断原先的Hash值新增的那一位是0还是1,若为0,则原先位置,若为1,则索引变为“oldCap+原索引”。

这地方有几点需要注意:

  1. 判断索引是否改变用的是 hash & oldCap,此处与计算索引值**(n - 1) & hash**需要区分开来
链表转化红黑树

treeifyBin()函数.在链表长度超过8的时候,使用该函数将链表转化为红黑树

其主要思路如下:

  1. 当整个数组桶的长度小于64时,此时并不会进行树化操作,只是进行扩容。注意,在进行扩容的时候,链表的长度有可能会变短
  2. 将链表中的节点转化为树节点TreeNode,形成一个新的链表
  3. 将新链表节点赋值给给定的数组项
  4. 调用TreeNode的treefify()方法将该处的链表转化为红黑树,该函数具体步骤如下:
  5. 插入树节点元素,具体可以参考二叉搜索树的插入操作。在进行插入的时候,比较key的hash值来决定插入的方向。
  6. 插入完成之后开始调整红黑树,使其符合红黑树的特性
查找

get函数的内部实现主要是getNode()函数,主要分析该函数

  1. 根据(n - 1) & hash计算得到该key所在的桶,若桶不存在或者数组table为空,则直接返回null.

  2. 当桶存在时,首先判断该桶上第一个节点是否就是要找的节点(大部分情况下都是),若是直接返回该节点值

  3. 若不是,判断节点类型,若是红黑树,则遍历红黑树查找,若是链表,则遍历链表查找

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值