HashMap扩容是2倍的原因(全网博客几乎都解释错了)

零、前言

最近在写博客时,突然又想起来哪个经常出现在面试题里的问题:

HashMap扩容为什么是原来的2倍?

因为看过源码,我觉得这个问题并不难。在我之前的通俗解释equals和hashCode的关系和作用里也说过这个原因。但为了博客的严谨性,所以还是查了一下,验证一下自己的观点。
结果发现网上各种类似的文章,都是稀里糊涂的解释这个问题,逻辑完全禁不起推敲。
所以我就单独写了这一篇博客,谈一下这个问题。
先说我的结论:
这只是为了适应一种快速散列方法做出的适当妥协和经验值
[看完本文如果还有不理解,或者不认同的,请留言交流]

一、网上的各种奇怪解释

类似逻辑不清晰的解释,网上很多,我就精选一些被大家广泛认同的观点:

扩容可以减少hash冲突(我精简了大量车轱辘话)

点评:这不废话吗,容量不够,hash冲突就会加剧,当然是为了减少hash冲突。这是在拿问题当结论了?人家问的是为什么是2倍的扩容。

2的次幂的容量可以让散列更均匀,减少hash冲突

点评:这显然是背八股文记混了,完全没有因果关系。属于发动了ChatGPT的胡说八道技能(大概率也能唬住面试官,反正面试要的就是个理直气壮的气势)。刨除车轱辘话,还是等于没讲

在这里插入图片描述

点评:这个说法很奇怪,脑洞也很大,是一个经过思考说法,但显然不太对

还有很多博客指出了关键点:hash % (n-1), 甚至还画了 &运算的图。
然后说:看,这样做hash运算之后可以让结果更均匀

点评:找到了关键点位置,但理解的稀里糊涂。

二、解释

我们先想一个问题:假如让你设计一个散列算法,要怎么设计?

2-1. 散列

所谓散列算法,就是把输入的值尽量均匀的分布在一段固定长度上面。在HashMap里就是散列到一段固定长度的数组里。
这里有两个关键点:

  1. 尽量均匀,也就是要充分利用空间
  2. 固定长度,也就是不能超过这个空间

如果没学过计算机,我觉得大家的一个朴素想法可能是:让输入的这个值经历一通复杂的计算过程(就像加密一样,加减乘除一通操作),这样输出的值就“足够乱”了,也就足够“均匀”了。

但我们学过计算机,就一眼就发现这种朴素想法的问题:这样是足够乱,但你怎么让它满足条件2呢,边界控制不住呀。
我们稍作思考,就很容易想到“取余运算”。也就是:xxx % n。 这也是我们开发中常用的分散数据+控边界的算法(比如负载均衡)。
我们先不管这个xxx怎么来的,单就对n取余余数 性能就不怎么稳定(随着容量变大,性能会越来越差)。作为一个设计良好的API,怎么会允许出现o(n)复杂度的计算呢,况且是使用频次如此高的类。

接着一开始那个朴素的想法,稍作完善,其实就是目前采用的散列算法:
用 “&运算”来控制这个足够乱的数的边界。
假如要控制的长度是16以内,就用二进制数: 0000 1111和这个值做&运算
因为&的计算特性,相当于把这个值截取了低4位。这四位数无论如何变化,也超不过16(最大15)
我们再观察就会发现,这个看起来很有规律的二进制,不就是 16 - 1吗。 这不巧了吗,16正好就是我们要控制的数据范围。我们的数据范围(16)居然还能变成一个二进制工具数,把任意数截取之后,都限制在数据范围(16)之内(妙哇~)。
再分析就会发现:并不是所有数 - 1都有这个特性。要形成 ...0001111...这样的数,必须由二进制...00000100000... 这样的数减去1 得到。
...00000100000... 这样的数就是 2 i 2^i 2i

2-2. 很乱的数

最后来再看前面被略过的“很乱的数”,想必大家都已经知道了,也就是对象的hash值。

如果不看前文,你是否会想用“随机数”充当这个很乱的数。
别搞混了,hash长得确实很随机,但不是随机数。随机数的意思是每次得到的值都毫无规律。而我们要的是输入一个值,返回值是个确定的数,只不过这个数看起来很乱,很随机。
和MD5或者加密算法类似

2-3. 为什么截取低位

直观的看一下hashcode长什么样就明白了

    public static void testHashCode() {
        System.out.println("abcde".hashCode());
        System.out.println("abcdf".hashCode());
        System.out.println("abcdg".hashCode());
    }

输出

92599395
92599396
92599397

这几个字符串hashcode的高位(前面几位)都是重复的,只有最后一位不同。只有截取低位时才能区分开来,如果大部分hash截取之后都一样,就会产生大量的hash冲突。
注意:上面展示的是这几个字符串对象的十进制hashcode,他们的二进制同样是高位完全一样。

细心的读者可能会发现,HashMap里并不是直接使用object.hashCode(),而是用的下面这个方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
但其实这并不影响效果,结果依然是高位重复,大家可以自己试试。

所以我不认为和( 2 i 2^i 2i-1)做&运算是HashMap散列的一部分。在我看来这就是简单的对hash的截取操作,取对象的hash是jvm已经做好了的,而用&去“截取”则是一个极为快速的位运算操作。

三、思考

  1. 散列算法只有截取hash和使用%这两种思路吗
    我觉得未必。只是目前科研技术能想到的只有这两种方式。
    所以很多博客义正言辞的说:因为可以避免使用&运算,所以采用&运算
    搞得好像已经数学证明散列算法只有这两种思路似的。
  2. 如果不考虑散列问题。简单乘以2的扩容真的是一个好方案吗
    肯定不是。简单想想就会明白,扩容的速度在呈指数级增长,只是从长期的经验值上看,这种方案还可以。并没有因此产生严重的问题。毕竟重新散列也是很费时间的,所以一次性多扩容也有道理。但这种粗犷的增长方式,空间浪费是必然的。所以我说:这是一种为了一种巧妙快速的散列算法做出的适当妥协。如果不考虑散列问题,我相信JDK开发团队能做出更精细化的扩容方案。

四、总结

  • 以目前的算法技术,当HashMap的容量为2的幂次方时,可以简单巧妙的实现快速截取hash值作为散列值的效果,从而避免了使用%运算做散列 这样大量的时间消耗。
  • 而散列的均匀不均匀,是对象hashcode本身的功能(功劳),这里只是对这个hashcode截取了一段有效(能区分对象)且能控制范围的值。

五、附录

附上ChatGPT给出的答案,我觉得还是比较准的:

在Java中,HashMap类使用2的幂扩容,以在调整大小操作期间实现更好的性能。当HashMap中的元素数量超过一定阈值时,底层数组(桶数组)会被调整大小以容纳更多元素。此调整大小操作是为了保持较低的负载因子,即元素数量与桶数量的比率。
2的幂扩容简化了调整大小期间重新散列的过程。当数组大小是2的幂时,哈希函数可以使用位与操作(hash & (length - 1))有效地计算,而不是使用非2的幂大小时的模运算(hash % length)。这个位与操作利用了数组大小是2的幂这一事实,确保了哈希码的低位被使用,从而实现更均匀地将键分布在桶之间。
以下是2的幂扩容有益的一些原因:
高效计算桶索引
使用2的幂大小时,模运算被位与操作取代,位与操作的计算成本更低。
例如,如果数组大小是16(2的4次方),表达式 hash & 15 可以高效地计算索引,等同于 hash % 16。
改善哈希码分布
在位与操作中使用哈希码的低位有助于实现更均匀的键分布。
简化代码
使用2的幂大小简化了计算桶索引的代码,使其更为简洁,潜在地更快速。
需要注意的是,虽然2的幂扩容提供了这些优势,但在某些情况下,它可能会导致哈希码的模式与2的幂大小对齐,从而产生更多的碰撞。然而,在实践中,简单性和性能之间的权衡通常倾向于使用2的幂扩容。此外,Java中HashMap的现代实现可能使用其他技术,例如树结构进行冲突解决,以处理碰撞成为问题的情况。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DesignDoll使用方法

选择材质球 取消网格线 控制手部动作-设置左右手 - 手部运动 控制身材 控制身高 比例

第三节TypeScript 基础类型

1、typescript的基础类型 如下表: 数据类型 关键字 描述 任意类型 any 生命any的变量可以赋值任意类型的值 数字类型 number 整数或分数 字符串类型 string 使用单引号(‘’)或者双引号(“”)来表示字符串…

RESTful简介与C/C++实现

一、RESTful简介 RESTful,全称为Representational State Transfer,是一种软件架构风格和设计理念,而不是一种标准。它主要用于Web服务的设计和开发,强调资源的状态表示和状态转移。RESTful风格的设计使得Web服务更加简洁、清晰和…

页面菜单,通过get请求一个url后,跳转另外一个页面,+丢失问题

业务场景描述: 在A系统,菜单点击跳B系统这个操作。 A系统菜单是get请求到B系统的一个缓冲页面,然后这个缓冲页面获取到url中的accessToken后,在这个页面中通过post请求后端接口。 问题描述: 当accessToken中包含了…

MongoDB 单机安装部署

文章目录 说明1. 下载安装包2. 安装数据库3. 配置 systemctl4. 创建 root 用户 说明 本篇文章介绍 MongoDB 二进制安装的步骤,整个过程还是比较简单。 1. 下载安装包 进入 MongoDB 官网,获取安装包的下载链接: https://www.mongodb.com/tr…

Leetcode—179.最大数【中等】

2023每日刷题&#xff08;六十五&#xff09; Leetcode—179.最大数 算法思想 实现代码 其中sort的lambda自定义排序策略参考自官方文档 class Solution { public:string largestNumber(vector<int>& nums) {string ans;vector<string> strs;for(auto num: …

mysql创建用户和赋权

1.创建用户 CREATE USER new_userlocalhost IDENTIFIED BY user_password; “localhost"只允许本地连接&#xff0c;而”%"允许所有IP地址都可以连接到服务器。 2.赋权 GRANT ALL PRIVILEGES ON database_name.* TO new_userlocalhost; FLUSH PRIVILEGES; 3.给…

Uniapp + Vue3 封装请求工具挂载全局

新建request.js工具类 const http {// baseUrl 地址baseUrl: http://localhost:8080,// 请求方法request(config) {// config&#xff1a;请求配置对象&#xff0c;具体参照uniapp文档config beforeRequest(config)// 请求地址拼接config.url this.baseUrl config.url// 异…

Linux Centos 配置 Docker 国内镜像加速

在使用 Docker 进行容器化部署时&#xff0c;由于国外的 Docker 镜像源速度较慢&#xff0c;我们可以配置 Docker 使用国内的镜像加速器&#xff0c;以提高下载和部署的效率。本文将介绍如何在 CentOS 系统上配置 Docker 使用国内镜像加速。 步骤一&#xff1a;安装 Docker 首…

最新时报!即将开业的新工厂将推出量产人形机器人

原创 | 文 BFT机器人 世界上第一个工厂批量生产的人形机器人即将在太平洋西北地区开业。 Agility Robotics首席执行官谢尔顿在一次采访中谈到&#xff0c;一旦达到“顶峰”&#xff0c;公司将在其“RoboFab”工厂生产10000个机器人&#xff0c;这就是量产的意思了&#xff0c;…

基础硬件、实施运维工程师与操作系统的介绍

目录 一、实施与运维 1.2 实施运维一般做什么 1.1.1实施工程师 1.1.2运维工程师 1.3 实施运维需要具备哪些技能 三、基础硬件 四、操作系统 4.1 Windows 4.2 Linux 4.3 macOS 4.4 Unix 五、总结 一、实施与运维 1.1 实施运维是干什么的 1、运维工程师负责服务的稳…

java 4.数组

文章目录 4.数组4.1数组的概念4.2 数组的定义4.3 数组的初始化4.4 数组下标的有效范围与常见异常4.5 数组内存分析4.6 二维数组4.6.1 创建二维数组4.6.2 二维数组的赋值4.6.3 多维数组4.6.4 通过二维数组输出不同版式的古诗 4.7 不规则数组4.8 数组的基本操作4.8.1 数组遍历4.8…

Leetcode 55 跳跃游戏

题意理解&#xff1a; 非负整数数组 nums, 最初位于数组的 第一个下标 。 数组中的每个元素代表你在该位置可以跳跃的最大长度。 需要跳到nums最后一个元素即为成功。 目标&#xff1a;是否能够跳到最后一个元素。 解题思路&#xff1a; 使用贪心算法来解题&#xff0c;需要理解…

神经网络:激活函数层知识点

1.激活函数的作用&#xff0c;常用的激活函数有哪些 激活函数的作用 激活函数可以引入非线性因素&#xff0c;提升网络的学习表达能力。 常用的激活函数 Sigmoid 激活函数 函数的定义为&#xff1a; f ( x ) 1 1 e − x f(x) \frac{1}{1 e^{-x}} f(x)1e−x1​ 如下图…

推荐几个好用的开源无代码/低代码开发平台

一、什么是无代码/低代码开发 无代码/低代码开发是一种可视化的应用程序开发方法&#xff0c;使用具有拖放组件和模型驱动逻辑组合的图形界面。无代码/低代码开发试图降低从软件技术平台、产品和服务中提取价值的进入壁垒。低代码开发平台被称为可视化集成开发环境&#xff08…

美颜技术详解:深入了解视频美颜SDK的工作机制

本文将深入探讨视频美颜SDK的工作机制&#xff0c;揭示其背后的科技奥秘和算法原理。 1.引言 视频美颜SDK作为一种集成到应用程序中的技术工具&#xff0c;通过先进的算法和图像处理技术&#xff0c;为用户提供令人印象深刻的实时美颜效果。 2.视频美颜SDK的基本工作原理 首…

node.js mongoose schemaTypes

目录 官方文档 简介 SchemaType 示例 配置SchemaType规则 通用规则 特定schemaType规则 String Number Date Map monggose会根据shcemaType将文档值转换成指定的类型 官方文档 Mongoose v8.0.3: SchemaTypes 简介 SchemaTypes是在使用Mongoose时&#xff0c;用于…

Elementor Pro 完整模板套件5000个登陆页面和1000个模板描说明

一、引言 Elementor Pro 是 WordPress 的一款强大且功能丰富的页面构建插件。它提供了完整的模板套件和登陆模板,使得用户能够轻松创建各种类型的页面和网站。本文将详细介绍 Elementor Pro 的完整模板套件和登陆模板的功能和使用方法。 二、Elementor Pro 完整模板套件 模板…

OSPF面试总结

OSPF 基本特点 属于IGP、LS支持无类域间路由没有环路&#xff08;区域内运行LS、区域间是DV,所以所有的区域要和区域0相连&#xff09;收敛速度快使用组播发送数据 224.0.0.5、224.0.0.6 什么时候用224.0.0.5&#xff1f;支持多条等价路由支持协议报文认证 OSPF路由的计算过程…

C#中的协变和逆变

这两个都是只能使用在接口和委托上 个人理解&#xff1a; 协变&#xff1a;出参&#xff0c;让基类使用范围变大&#xff0c;将父类/基类当作子类一样使用 --为什么这样规定呢&#xff1f; 我的理解&#xff1a;真正实现的是子类&#xff0c;子类拥有所有的方法&#xff0c;却…