HashMap确定key的存储位置的源码分析

 更新时间:2023年07月24日 10:42:48   作者:单程车票  
HashMap 作为 Java 中最常用的数据结构之一,用于存储和管理键值对,HashMap 基于哈希函数实现,能通过将 key 映射到特定的位置来实现快速存储、查找和删除数据,接下来将从源码角度分析以通俗易懂的方式向大家讲解一下 HashMap 如何确定 key 的存储位置的

前言

HashMap 通过哈希函数确定 key 的存储位置这一映射过程,可使得 HashMap 在常数时间内完成插入、查找和删除操作,那么大家是否有了解过 HashMap 底层是如何使用哈希函数实现映射的呢?是如何高效确认 key 的存储位置的呢?

接下来将从源码角度分析以通俗易懂的方式向大家讲解一下 HashMap 如何确定 key 的存储位置的。

源码分析

下面以 JDK 1.8 版本查看 HashMap 中最为常用的方法之一 put(K key, V value) 方法来看看 key 是如何确定存储位置的。

可以看到这里 key 会先通过 hash() 方法进行处理,进入 hash() 方法一探究竟。

扰动函数 #hash()

JDK 1.8 中的 hash() 方法

源码:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

第一次看到 (h = key.hashCode()) ^ (h >>> 16) 这个式子可能有点不太好理解,我们分开来看:

  • 可以看到式子中的 key.hashCode(),说明 HashMap 会将传入的 key 调用自身父类 ObjecthashCode() 来进行哈希计算。
  • 然后将计算得到的哈希值 h 通过 h ^ (h >>> 16) 把高 16 位异或(^)低 16 位形成新的哈希值(hashCode() 方法返回的是 int 类型,也就是 32 位的数据)。

(h = "key".hashCode() ^ (h >>> 16)) 为例:

将计算后的哈希值的高 16 位与低 16 位异或(^)的目的是为了通过把原本的低 16 位变成高 16 位和低 16 位的混合结果来使得低 16 位的随机性增大,这样在数组长度较小时,能起到保证高 16 位也参与到 Hash 计算中(这个在后面的取模操作中很好的体现),同时不会造成太大的性能开销。

JDK1.8 中 HashMap 的 hash() 方法也叫做扰动函数,其目的就是为了增加随机性,让数据元素更加均衡的散列,减少碰撞。

补充 JDK 1.7 的 hash() 方法

static int hash(int h) {
    int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK 1.7 中对 hashCode() 方法计算出的哈希值进行了四次扰动,所以在性能上要差一点。在 JDK 1.8 对其优化了扰动算法,使得计算的性能开销降低许多。这里也是 JDK 1.7 和 1.8 在计算 key 的存储位置上不同的地方了。

取模操作

在扰动函数 hash() 方法中可以看到返回的是一个 int 类型的值,即值的范围在 [-2147483648, 2147483647] 内,也就是说通过 hash() 映射到的位置可以在近 40 亿的长度的数组中。

但是实际上的内存不允许数组达到这么大,那么 HashMap 是如何将这个哈希值映射到数组中呢,相信读者们的第一反应一定是取模了,那么 HashMap 是怎样进行取模操作的呢,深入 putVal() 方法可以看到:

可以看到方法中通过 (n - 1) & hash 来确定最终 key 的存储位置,其实 (n - 1) & hash 这个式子就是取模操作,把通过 hash() 方法计算后得到的 hash 和当前 HashMap 的数组长度 - 1 进行与运算,相当于 hash % n,从而将其映射到数组特定的索引中。那么为什么 (n - 1) & hash 等价于 hash % n 呢?

其实这个等价关系有一个必要的前提,这个前提就是 n 总是 2n2^n2n,即数组的长度总是 2 的幂次方。当数组长度为 2 的幂次方时,可以保证 (n - 1) & hash 等价于 hash % n

这是因为当 n 为 2 的幂次方时,n - 1 可以保证高位为 0,低位全 1 的效果,所以,按位与运算的结果相当于保留了 hash 的二进制表示的低位部分,而将高位部分全部置为 0,从而确保了 (n - 1) & hash 等价于 hash % n

举个例子:上图可以知道 "key" 的哈希值计算结果为 0000 0000 0000 0001 1001 1110 0101 1110 即 106078,假设当前数组长度为 16 即 0001 0000,那么正常取模为 106078 % 16 = 14

106078 & (16 - 1) 结果如图:

至于为什么要使用位运算呢?这是因为取模运算的性能开销较大,替换成位运算可以得到更高的计算效率,提高性能。并且数组长度为 2 的幂次方可以起到散列均匀分布,减少哈希碰撞的可能性。

总结

上面就是 HashMap 如何确定 key 的存储位置的整个源码分析过程了,过程可以分为三步:

  • 将传入的参数 key 调用自身的方法 hashCode() 得到哈希值 h。
  • 根据哈希值 h 调用扰动函数 hash() 计算 h ^ (h >>> 16) 得到扰动后的哈希值 hash。
  • 根据哈希值 hash 取模操作 hash & (n - 1) 从而确定 key 的存储位置。

以上就是本篇文章的全部内容了。

到此这篇关于HashMap确定key的存储位置的源码分析的文章就介绍到这了,更多相关HashMap确定key位置内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java四种常用线程池的详细介绍

    Java四种常用线程池的详细介绍

    今天小编就为大家分享一篇关于Java四种常用线程池的详细介绍,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-03-03
  • SpringBoot两种方式接入DeepSeek的实现

    SpringBoot两种方式接入DeepSeek的实现

    本文主要介绍了SpringBoot两种方式接入DeepSeek的实现,包括HttpClient方式和基于spring-ai-openai的方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-03-03
  • springboot整合httpClient代码实例

    springboot整合httpClient代码实例

    这篇文章主要介绍了springboot整合httpClient代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • MyBatisPlus+Spring实现声明式事务的方法实现

    MyBatisPlus+Spring实现声明式事务的方法实现

    本文主要介绍了MyBatisPlus+Spring实现声明式事务的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-07-07
  • 一文了解SpringBoot是如何连接数据库的

    一文了解SpringBoot是如何连接数据库的

    Spring Boot提供了一系列的开箱即用的功能和特性,使得开发人员可以快速构建和部署应用程序,下面这篇文章主要给大家介绍了关于SpringBoot是如何连接数据库的相关资料,需要的朋友可以参考下
    2023-06-06
  • Java中的functor实现

    Java中的functor实现

    Java中的functor实现...
    2006-12-12
  • springcloud初体验(真香)

    springcloud初体验(真香)

    这篇文章主要介绍了springcloud初体验(真香),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  •  Java数据结构的十大排序

     Java数据结构的十大排序

    这篇文章主要介绍了 Java数据结构的十大排序,排序算法分为比较类排序和非比较类排序,具体的内容,需要的朋友参考下面思维导图及文章介绍,希望对你有所帮助
    2022-01-01
  • java父子节点parentid树形结构数据的规整

    java父子节点parentid树形结构数据的规整

    这篇文章主要介绍了java父子节点parentid树形结构数据的规整,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • Java两种方法计算出阶乘尾部连续0的个数

    Java两种方法计算出阶乘尾部连续0的个数

    这篇文章主要介绍了Java两种方法计算出阶乘尾部连续0的个数,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03

最新评论