HashMap源码解析-jdk1.8(三)

news2024/11/26 21:53:15

  • HashMap源码解析-jdk1.8(三)
    • 负载因子loadFactor为什么是0.75?
    • HashMap的长度为什么是2的幂次方
      • 1. 与取余等价的算法
      • 2. 扩容时方便定位
      • 总结

HashMap源码解析-jdk1.8(三)

负载因子loadFactor为什么是0.75?

/**
 * 默认的负载因子,用来衡量HashMap满的程度。
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

负载因子,用来衡量HashMap满的程度。通常,默认负载因子(0.75)在时间和空间成本之间提供了一个很好的权衡。负载因子越大则散列表的装填程度越高,减少空间开销,但会增加查找成本。负载因子越小则链表中的数据量就越稀疏,此时会对空间造成烂费,但是此时索引效率高。

试想一下,如果我们把负载因子设置成1,容量使用默认初始值16,那么表示一个HashMap需要在"满了"之后才会进行扩容。那么在HashMap中,最好的情况是这16个元素通过hash算法之后分别落到了16个不同的桶中,否则就必然发生哈希碰撞。而且随着元素越多,哈希碰撞的概率越大,查找速度也会越低。

我们假设一个bucket空和非空的概率为0.5,我们用s表示容量,n表示已添加元素个数。用s表示添加的键的大小和n个键的数目。根据二项式定理,桶为空的概率为:

请添加图片描述

因此,如果桶中元素个数小于以下数值,则桶可能是空的:
请添加图片描述

当s趋于无穷大时,如果增加的键的数量使P(0) = 0.5,那么n/s很快趋近于ln(2)(约等于0.6931…),所以,合理值大概在0.7左右。

通过一个数学推理,测算出这个数值在0.7左右是比较合理的。那么,为什么最终选定了0.75呢?还记得前面我们提到过一个公式吗,就是临界值(threshold) = 负载因子(loadFactor) * 容量(capacity)。其中capacity的值永远都是2的幂,为了保证threshold是一个整数,这个值是0.75(3/4)比较合理,因为这个数和任何2的幂乘积结果都是整数。

数组桶中的链表长度大于8就会转化为红黑树,既然转化为红黑树,那么treeNode占用的空间就是普通链表Node占用空间的两倍大,这样空间上会造成损失,为了避免这样的损失,经过研究计算发现,在随机hash的时候,数组桶内的Node元素分布呈现泊松分布。既然是泊松分布,那么我们就需要一个阈值,尽可能的避免treeNode的出现,这时候负载因子出场了,负载因子为0.75的时候,桶中的Node的分布频率服从参数为0.5的泊松分布。桶内达到8以上的元素几乎不可能(一亿分之6的概率),这样就避免了TreeNode的出现。

  • 0: 0.60653066
  • 1: 0.30326533
  • 2: 0.07581633
  • 3: 0.01263606
  • 4: 0.00157952
  • 5: 0.00015795
  • 6: 0.00001316
  • 7: 0.00000094
  • 8: 0.00000006
  • more: less than 1 in ten million

当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。

HashMap的长度为什么是2的幂次方

1. 与取余等价的算法

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。为了减少hash值的碰撞,需要实现一个尽量均匀分布的hash函数,在HashMap中通过利用key的hashcode值,来进行位运算。
请添加图片描述

这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。

/*
 * jdk1.8 & jdk1.7
 * 从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,
 * 也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
 */
static final int hash(Object key) {
    int h;
    // h = key.hashCode() 为第一步 取hashCode值
    // h ^ (h >>> 16)  为第二步 高位参与运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/*
 * jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
 */
 static int indexFor(int h, int length) { 
     return h & (length-1);  //第三步 取模运算
}

把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的。h & (length-1)就是取模操作,Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。

那么,为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:

取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash % length == hash & (length-1)的前提是 length 是 2 的 n 次方),这就解释了 HashMap 的长度为什么是 2 的幂次方。

X % 2n = X & (2n - 1)
2n: 表示2的n次方,也就是说,一个数对2n取模 == 一个数和(2n - 1)做按位与运算 。
假设n为3,则23 = 8,表示成2进制就是1000。23 -1 = 7 ,即0111。
此时X & (23 - 1) 就相当于取X的2进制的最后三位数。
从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。

举个例子:

假如 length = 24 = 16,二进制10000。这个数减去1的结果是1111,也就是length -1 = 1111。 (下面这段中的数字都是二进制) 再假设一个key的hash值为10011011001,与1111做 & 操作,得到的结果是1001(高位部分1001101都舍去了)。而1001必然是一个小于10000的数,对于一个小于10000的数而言,1001 % 10000得到的就是1001自己。

那么刚刚舍弃的高位部分1001101 0000(后面补上了四个0000)就一定能被10000整除吗?答案是肯定的:因为10011010000可以拆成10000000000+10000000+1000000+10000,这几个数都能通过10000的n次左移得到,也就相当于这几个数都能被10000整除。那他们的和,也就是10011010000,一定也可以被10000整除。 因此,最终结论就是:10011011001 & ( 10000 - 1 ) = 10011011001 & 1111 = 1001 = 10011011001 % 10000
请添加图片描述

2. 扩容时方便定位

当hashmap需要扩容,重新计算链表元素的hashcode,以进行元素的重新定位时,依然能从“ 数组2次幂 ”的这个设定中借力! hashmap数组扩容时,新数组length = 原数组length * 2,沿用前面的例子(length = 24 = 16,二进制10000),length 乘以 2 ,即二进制左移一位,由 10000 变成 100000。此时需要重新计算数组槽中的元素位置,如果槽中是链表,链表中每个元素都需要重新计算位置(这里不考虑红黑树)。

计算的公式不变,key.hashcode & (array.length - 1),由于数组的翻倍(10000->100000),导致 array.length - 1 发生了改变(1111->11111)。此时,扩容前原本被舍弃的高位部分的最后1位,也将参与计算。
请添加图片描述

在扩容这里,这一位就显得很特别:如果这个位置是0,余数计算的结果将保持不变,意味着扩容后此元素还在这个槽中(槽编号没发生改变);如果这个位置是1,余数计算结果就变成了原槽索引 + 原length。也就是说,hashmap扩容的元素迁移过程中,由于数组大小是2次幂的巧妙设定,使得只要检查 “ 特殊位 ” 就能确定该元素的最终定位。
请添加图片描述

**扩容前:**红绿黄三个元素,由各自的hashcode取余后都淤积在数组槽13,组成链表形式。

**扩容后:**红、绿二星所表示的元素的hashcode“ 特殊位 ”为0,取余依然定位在槽13;而黄星表示的元素,hashcode“ 特殊位 ”为1,取余后结果 = 原槽索引 + 原数组大小 = 13 + 16 = 29。(这个结果也和图中黄星的hashcode二进制低位值11101一致)。

总结

对hashmap而言,数组长度始终保持2次幂有两点好处:

  1. 能利用 & 操作代替 % 操作,提升性能
  2. 数组扩容时,仅仅关注 “特殊位” 就可以重新定位元素

参考:
https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap
http://hollischuang.gitee.io/tobetopjavaer/#/basics/java-basic/hash-in-hashmap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1267279.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【深度解析】Jmeter性能测试指标的重要性与实现方法!

通常情况下,性能测试关注被测对象的时间与资源利用特性及稳定性。时间特性,即被测对象实现业务交易过程中所需的处理时间,从用户角度来说,越短越好。资源利用特性,即被测对象的系统资源占用情况,一般Web系统…

【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇

目录 前言版本环境变量配置引入的类型1、AxiosIntance: axios实例类型2、InternalAxiosRequestConfig: 高版本下AxiosRequestConfig的拓展类型3、AxiosRequestConfig: 请求体配置参数类型4、AxiosError: 错误对象类型5、AxiosResponse: 完整原始响应体类型 目标效果开始封装骨架…

Mac自动同步微信聊天记录(Mac显示资源库)

Mac自动同步微信聊天记录 在使用阿里云盘自动同步mac上微信的聊天记录时,遇到了/home/wangguagnjie/目录下没有资源库文件夹的情况 需要按照以下步骤将其显示到/home/用户名目录下,才能选中指定文件夹 使用阿里云盘,可以选择自动同步指定文…

ModBus电表与RS485电表有哪些区别?

在能源计量领域,ModBus电表和RS485电表是两种常见的设备,它们都具有监测和记录电能数据的功能。然而,它们之间存在一些区别,比如通信协议、连接方式、数据格式等等参数的区别有哪些? ModBus电表和RS485电表都是用于电能…

Java多线程其他细节知识

并发、并行 进程 并发的含义 并行的理解 线程的生命周期

奇葩问题:arp缓存与ip地址冲突(实际是ip地址被占用导致arp缓存出现问题)

文章目录 今天遇到个奇葩的问题 今天遇到个奇葩的问题 今天遇到个奇葩的问题,我把我们192.168.1.116的盒子ip改成192.168.2.116后,再改回来,发现我们盒子的http服务始终无法访问,用Advanced IP Scanner扫描一下,发现就…

虾皮选品分析工具:为卖家提供市场洞察和优化策略

随着电商平台的发展,越来越多的卖家选择在虾皮(Shopee)平台上销售产品。然而,如何在激烈的竞争中脱颖而出,成为卖家们面临的一大挑战。虾皮选品分析工具应运而生,为卖家提供了市场分析、选品策略和产品优化…

RHCSA---基本命令使用

文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据总结 前言 Linux中终端中的很多操作都是通过命令行实现的,最常用的输入命令的方法有以下两种。 (1).打开自带的终端,类似于Windows中的CMD (2).ssh远程连接,关于…

VUE2中使用阿里云播放器AliPlayer

简述 基于 Vue 的播放器单页应用, 利用 web 播放器 sdk 进行视频点播,包含播放列表、字幕、多语言、自适应码率,皮肤自定义等功能 Web播放器文档 已知问题 vue中使用截图,不太好使【已自行优化】无键盘快捷键,无法通过空格暂停…

力扣:184. 部门工资最高的员工(Python3)

题目: 表: Employee ----------------------- | 列名 | 类型 | ----------------------- | id | int | | name | varchar | | salary | int | | departmentId | int | ----------------------- 在 SQL …

人工智能原理复习--知识表示(二)

文章目录 上一篇产生式表示法推理方式 结构化表示语义网络语义网络表示知识的方法和步骤应用题目 框架表示法下一篇 上一篇 人工智能原理复习–知识表示(一) 产生式表示法 把推理和行为的过程用产生式规则表示,所以又称基于规则的系统。 产…

9.二维数组——打印出杨辉三角形(要求打印出10行)

文章目录 前言一、题目描述 二、题目分析 三、解题 程序运行代码 前言 本系列为二维数组编程题&#xff0c;点滴成长&#xff0c;一起逆袭。 一、题目描述 打印出杨辉三角形&#xff08;要求打印出10行&#xff09;。 二、题目分析 三、解题 程序运行代码 #include<s…

基于SpringCloud的动漫论坛

基于SpringCloud的动漫论坛《BOKI》 摘要&#xff1a;鉴于现如今的互联网网站的存在形式&#xff0c;网站内部有可能内嵌论坛&#xff0c;因此&#xff0c;该项目中实现一个整体的、可移植性强的插件式论坛&#xff0c;论坛就有可能突破ACG主题的限制&#xff0c;实现论坛与主…

亚马逊发布人工智能助手Amazon Q,一起来看看有什么功能

Amazon 在11.28日Re:Invent大会上推出人工智能助手Amazon Q&#xff0c;主要面向企业客户&#xff0c;提供个性化服务。号称是专为工作定制的生成式人工智能助手。Your generative Al-powered assistant tailored for work 核心能力企业知识库&#xff1a;为客户提供快速、相关…

YOLOv8独家原创改进:自研独家创新MSAM注意力,通道注意力升级,魔改CBAM

💡💡💡本文自研创新改进:MSAM(CBAM升级版):通道注意力具备多尺度性能,多分支深度卷积更好的提取多尺度特征,最后高效结合空间注意力 1)作为注意力MSAM使用; 推荐指数:五星 MSCA | 亲测在多个数据集能够实现涨点,对标CBAM。 在道路缺陷检测任务中,原始ma…

oracle官方的反解析工具:javap详解

1、解析字节码的作用 通过反编译生成的字节码文件&#xff0c;我们可以深入的了解java代码的工作机制。但是&#xff0c;自己分析类文件结构太麻烦了&#xff01;除了使用第三方的jclasslib工具之外&#xff0c;oracle官方也提供了工具&#xff1a;javap javap是jdk自带的反解…

laraval6.0 GatewayWorker 交互通信

laravel 6.0 GatewayWorker 通讯 开发前准备下载 GatewayWorker 及操作方式前端demo测试效果项目中安装GatewayClient开发前准备 GatewayClient 官网:https://www.workerman.net/ 当前使用的是宝塔操作 下载 GatewayWorker 及操作方式 前端demo 测试效果 项目中安装GatewayC…

Windows快速找到软件的exe文件路径

Windows快速找到软件的exe文件路径 你是否有如下困惑&#xff1a; 经常忘记软件的默认安装路径忘记了软件自定义的安装路径 以至于无法找到软件启动的可执行程序exe文件&#xff1f; 实际中&#xff0c;在Windows系统中使用pywinauto时经常需要去寻找软件启动的可执行文件ex…

从零开始的c语言日记day38——数组参数,指针参数

一维数组传参 要把数组或者指针传给函数&#xff0c;那函数参数如何设计&#xff1f; 上面各写法有问题嘛&#xff1f; 第一个没问题 第二个没问题 第三个没问题 第四个没问题 第五个解析&#xff1a;定义int*arr2[20]为20个int*类型的数组&#xff0c;test2之后用的是ar…

python项目报错

解决办法&#xff1a;不要用配置的镜像脚本&#xff0c;直接用此命令 pip install pandas -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com