深入理解HashMap(及hash函数的真正巧妙之处)

2023-11-16

Hashmap是一种非常常用的、应用广泛的数据类型,最近研究到相关的内容,就正好复习一下。网上关于hashmap的文章很多,但到底是自己学习的总结,就发出来跟大家一起分享,一起讨论。

1、hashmap的数据结构
要知道hashmap是什么,首先要搞清楚它的数据结构,在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,hashmap也不例外。Hashmap实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“),请看下图(横排表示数组,纵排表示数组元素【实际上是一个链表】)。




从图中我们可以看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组。我们来看看java代码:

Java代码 复制代码
  1. /**  
  2.      * The table, resized as necessary. Length MUST Always be a power of two.  
  3.      *  FIXME 这里需要注意这句话,至于原因后面会讲到  
  4.      */  
  5.     transient Entry[] table;  
/**
     * The table, resized as necessary. Length MUST Always be a power of two.
     *  FIXME 这里需要注意这句话,至于原因后面会讲到
     */
    transient Entry[] table;

 

Java代码 复制代码
  1. static class Entry<K,V> implements Map.Entry<K,V> {   
  2.         final K key;   
  3.         V value;   
  4.         final int hash;   
  5.         Entry<K,V> next;   
  6. ..........   
  7. }  
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        final int hash;
        Entry<K,V> next;
..........
}



        上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。
         当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的,但是理想总是美好的,现实总是有困难需要我们去克服,哈哈~

2、hash算法
我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。

所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的,

Java代码 复制代码
  1. static int indexFor(int h, int length) {   
  2.        return h & (length-1);   
  3.    }  
 static int indexFor(int h, int length) {
        return h & (length-1);
    }



首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。

         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!





          所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
          说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。

所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):

Java代码 复制代码
  1. // Find a power of 2 >= initialCapacity   
  2.         int capacity = 1;   
  3.         while (capacity < initialCapacity)    
  4.             capacity <<= 1;  
// Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;





3、hashmap的resize

       当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

         那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。


4、key的hashcode与equals方法改写
在第一部分hashmap的数据结构中,annegu就写了get方法的过程:首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。所以,hashcode与equals方法对于找到对应元素是两个关键方法。

Hashmap的key可以是任何类型的对象,例如User这种对象,为了保证两个具有相同属性的user的hashcode相同,我们就需要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就可以找到在hashmap数组中的位置了。如果这个位置上有多个元素,还需要用key的equals方法在对应位置的链表中找到需要的元素,所以只改写了hashcode方法是不够的,equals方法也是需要改写滴~当然啦,按正常思维逻辑,equals方法一般都会根据实际的业务内容来定义,例如根据user对象的id来判断两个user是否相等。
在改写equals方法的时候,需要满足以下三点:
(1) 自反性:就是说a.equals(a)必须为true。
(2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。
(3) 传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。
通过改写key对象的equals和hashcode方法,我们可以将任意的业务对象作为map的key(前提是你确实有这样的需要)。


总结:
        本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才可以说对HashMap有了一定的理解。

 

来源:http://www.iteye.com/topic/539465

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入理解HashMap(及hash函数的真正巧妙之处) 的相关文章

随机推荐

  • 项目范围和项目范围管理

    项目的范围包括项目的最终产品或服务以及实现该产品或服务所需要开始的各项具体工作 1 项目产品范围 项目所要生产的产品或服务的特征和功能 2 项目工作范围 项目范围管理是指为了成功完成项目 对项目工作包括什么与不包括什么的定义与控制过程 项目
  • python实现md5加密

    python实现MD5加密 1 简介 Message Digest Algorithm MD5 中文名为消息摘要算法第五版 为计算机安全领域广泛使用的一种散列函数 用于确保信息传输完整一致 MD5是单向加密 指只能加密数据而不能解密数据 主
  • 【FAQ】TheBrain最新最热问答集锦

    TheBrain是一款与众不同的思维导图软件 其所有信息通过一个又一个的节点进行联系 最终形成一个杂而不乱的网状结构 TheBrain功能亮点 数字思维 整合分散的资源 随时访问 快速搜索 强大的文档管理功能 一目了然的图形 最新版TheB
  • 去阿里面试到第三轮的时候,会如何360度无死角考察你?

    V xin ruyuanhadeng获得600 页原创精品文章汇总PDF 目录 1 业务背景介绍 2 架构演进考察 3 对公司底层技术的原理考察 4 系统难点的考察 5 擅长技术的考察 6 总结 这篇文章 给大家分享一个同学面试阿里某个部门
  • 回归分析的基本步骤与自相关性

    一个回归分析的步骤 1 用scat x y 查看散点图 2 使用适当的模型进行回归分析 ls 估计出参数 3 统计检验 包括拟合优度检验和模型显著性检验 4 时间序列数据要做自相关性分析 横截面数据做异方差性检验 4 检查是否有多重共线性
  • Linux环境Ubuntu源码部署odoo15

    部署之前首先升级apt 否则所有的包都无法获取 sudo apt get update PostgreSQL相关 安装数据库 sudo apt get install postgresql 安装数据库客户端 sudo apt get ins
  • Java实现一个简单的命令行聊天程序

    Socket编程 Java实现 客户端 package net import java io BufferedReader import java io InputStreamReader import java io PrintWrite
  • SaaS、ERP、CRM、PaaS、IaaS概念

    layout post title SaaS ERP CRM PaaS IaaS概念 categories 实施工程 tags SaaS SaaS Software as a Service 软件即服务 SaaS首先是一个模式 其次也可理解
  • 开发支付宝小程序对比开发微信小程序

    接触使用开发支付宝小程序 之前弄微信小程序现在也做一点点支付宝小程序的需求 发现支付宝小程序和微信小程序这两者还有很多东西是共通的 当然也有很多差异 本文旨在在自己接触的过程中进行对比总结 会一直更新比较 1 开发工具 提供功能 开发工具上
  • redis专题-----11-----redis订阅发布以及stream

    参考文章 Redis发布订阅模式 publish subscribe 一 订阅发布 1 概念 1 为了支持消息的多播机制 redis 引入了发布订阅模块 Redis发布 订阅 Pub Sub 是一种通信机制 将数据推到某个信息管道中 其他客
  • 前端不规则气泡图实现方案

    背景介绍 由于项目大屏可视化的需求 需要实现在一个不定长宽的长方形容器中 实现不重叠分布的气泡图 每个气泡代表一类数据的统计值 气泡个数最大值已知 气泡大小与数据值大小正相关 并且气泡图需要有浮动特效 页面尺寸改变时 气泡尺寸需要自适应容器
  • 微信小程序0基础到精通

    写在开头 此篇文章只是简单描述了一下一条学习路线 具体学习资源获取方式请看文末 第一步 想快速入门小程序开发就需要从必备的API和核心开发等基础知识入手 建立整个小程序的知识体系 学习目标 新手入门与项目实战 课程重点 1 小程序核心技术
  • 计算机原理提问,计算机原理的计算机原理人话版

    Windows关机步骤涉及到Windows多个组件和多个过程 简单的说 Windows的关机步骤不是大多数人认为的那么简单 基本的过程是这样的 1 用户发起关机指令以后 发起关机指令的程序会通知Windows子系统CSRSS EXE CSR
  • 提高ChatGPT稳定性:告别GPT网页登录使用PC软件进入GPT

    一 GPT时不时断线 在日常生活中 我们经常需要使用智能语言模型来辅助我们完成各种任务 而ChatGPT作为一款非常优秀的智能语言模型 被广泛应用于各个领域 然而 使用ChatGPT的过程中 我们不可避免地会遇到一个非常让人头疼的问题 Ch
  • javaee之黑马乐优商城4

    商品规格与数据结构 下面来说一下数据库的设计与分析 其实对于spu这张表来说 大体设计还是比较好设计的 看一下下面这张图是一个产品的规格参数 上面主体就是一个规格参数 基本信息又是一个规格参数 这里就是涉及到了一个商品的具体信息 sku就是
  • Android 中的CreatePackageContext()

    Return a new Context object for the given application name This Context is the same as what the named application gets w
  • 分享一个OJ平台——浙江工商大学的OJ平台

    1 引言 最近是有总喜欢讨论算法题 因为他们在准备考研复试 为什么我不准备呢 这是一个悲伤的故事 刚好自己也有面试遇到只能使用C和C 的代码题 他们说这OJ平台相对简单一些 那些刷不来LeetCode可以试试这个 作为入门算法的跳板 体验体
  • Opencv中copyTo()函数的使用方法

    在Mat矩阵类的成员函数中copyTo roi mask 函数是非常有用的一个函数 尤其是后面的mask可以实现蒙版的功能 我们用几个实例来说明它的作用 我们要注意mask的数据类型 必须是CV 8U 且通道数或者是1 或者与roi一致 首
  • CloudCompare 二次开发(14)——高斯滤波

    目录 一 概述 二 代码集成 三 结果展示 一 概述 使用CloudCompare与PCL的混合编程实现点云高斯滤波 高斯滤波的算法原理见 PCL 高斯滤波 二 代码集成 1 mainwindow h文件public中添加 void doA
  • 深入理解HashMap(及hash函数的真正巧妙之处)

    Hashmap是一种非常常用的 应用广泛的数据类型 最近研究到相关的内容 就正好复习一下 网上关于hashmap的文章很多 但到底是自己学习的总结 就发出来跟大家一起分享 一起讨论 1 hashmap的数据结构 要知道hashmap是什么