【Java集合篇】HashMap 是如何扩容的

news2024/11/16 10:44:30

在这里插入图片描述

HashMap 是如何扩容的

  • ✔️ 为什么需要扩容?
  • ✔️ 桶元素重新映射
  • ✔️链表重新链接
  • ✔️ 取消树化
  • ✔️拓展知识仓
    • ✔️除了rehash之外,哪些操作也会将树会退化成链表?


✔️ 为什么需要扩容?


HashMap在Java等编程语言中被广泛使用,用于存储键值对数据。HashMap的实现原理是基于哈希表,通过哈希函数将键转化为桶的位置,从而实现快速查找、插入和删除操作。


然而,当HashMap中的元素数量增加时,哈希冲突的概率也会随之增加,这可能导致HashMap的性能下降。具体来说,当一个桶中的元素过多时,查询、插入和删除操作的时间复杂度会变为O(n),这违背了HashMap设计的初衷。为了解决这个问题,HashMap会在必要时进行扩容。


扩容的目的是为了增加HashMap的容量,从而减少哈希冲突的概率,提高查询、插入和删除操作的性能。扩容的过程涉及到重新计算所有元素的哈希值和重新分配元素到新的桶中,这是一个相对耗时的操作。因此,为了平衡性能和扩容的开销,通常会将HashMap的容量设定为一个较大的初始值,并在需要时进行扩容。


另外,扩容还可以帮助维持HashMap的性能稳定性。由于负载因子是存储元素的数量与哈希表容量的比值,当负载因子过高时,哈希表的性能会下降。通过适时地扩容,可以控制负载因子在一个合适的范围内,从而保证HashMap的性能。


HashMap需要扩容是为了减少哈希冲突的概率,提高查询、插入和删除操作的性能,以及维持性能稳定性。


当我们在Java 中使用 HashMap 时,可以通过以下代码示例来理解扩容的原理:

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // 创建一个初始容量为16,负载因子为0.75的HashMap
        HashMap<String, Integer> map = new HashMap<>(16, 0.75f);

        // 添加元素到HashMap中
        map.put("Alice", 25);
        map.put("Bob", 30);
        map.put("Charlie", 35);
        map.put("David", 40);

        // 输出当前HashMap的容量和已使用容量
        System.out.println("Initial capacity: " + map.capacity());
        System.out.println("Used buckets: " + map.size());

        // 添加更多元素,触发扩容
        map.put("Eve", 45);
        map.put("Frank", 50);
        map.put("Grace", 55);
        map.put("Henry", 60);

        // 再次输出当前HashMap的容量和已使用容量
        System.out.println("Current capacity: " + map.capacity());
        System.out.println("Used buckets: " + map.size());
    }
}

在上述代码中,我们创建了一个初始容量为16,负载因子为0.75的HashMap。当我们向HashMap中添加元素时,如果已使用容量超过了负载因子与当前容量的乘积,HashMap就会触发扩容。扩容后,HashMap的容量会增加,从而减少哈希冲突的概率,提高查询、插入和删除操作的性能。


参考前两篇博文:

【Java集合篇】负载因子和容量的关系
【Java集合篇】为什么HashMap的Cap是2^n,如何保证?


假设现在散列表中的元素已经很多了,但是现在散列表的链化已经比较严重了,哪怕是树化了,时间复杂度也没有O(1)好,所以需要扩容来降低Hash冲突的概率,以此来提高性能。


我们知道,当 ++size >threshold 之后详见java.util.HashMap#putVal 方法),HashMap就会初始化新的新的桶数组,该桶数组的size为原来的两倍,在扩大桶数组的过程中,会涉及三个部分:


1 . 如果某桶节点没有形成链表,则直接rehash到其他桶中


2 . 如果桶中形成链表,则将链表重新链接


3 . 如果桶中的链表已经形成红黑树,但是链表中的元素个数小于6,则进行取消树化的操作


✔️ 桶元素重新映射


如果桶中只有一个元素,没有形成链表,则将原来的桶引用置为null,同时,将该元素进行 rehash 即可,如下代码所示:


if (e.next == null) {
	newTab[e.hash & (newCap - 1)] = e;
}

在Java的HashMap中,当元素数量增加到一定阈值时,为了提高性能和减少哈希冲突,会触发桶的重新映射(rehashing)。这是通过扩容来实现的。

桶的重新映射过程涉及到以下步骤:

  1. 计算新的容量:扩容时,新的容量通常是旧容量的一个固定倍数(例如,默认情况下是原容量的两倍)。
  2. 生成新的桶数组:根据新的容量,创建一个新的桶数组。这个数组的长度是原来的两倍(在默认情况下)。
  3. 重新计算哈希值:遍历旧桶中的每个元素,并使用新的哈希函数重新计算它们的哈希值。这是为了确保元素能够均匀分布在新的桶数组中,减少哈希冲突。
  4. 重新放置元素:根据新计算的哈希值,将每个元素放置在新的桶数组中的适当位置。
  5. 更新HashMap状态:更新HashMap的容量、大小以及相关的内部状态。

这个过程是必要的,因为随着元素的增加,哈希冲突的概率也会增加,导致性能下降。通过扩容和重新映射,可以维持HashMap的高效性能。


✔️链表重新链接


假设有4个key,分别为a,b,c,d,且假定他们的hash值如下:


hash(a) = 3;  hash(a) & 7 = 3;    hash(a) & 8 = 0;
hash(b) = 11;  hash(b) & 7 = 3;   hash(b) & 8 = 8;
hash(c) = 27;  hash(c) & 7 = 3;   hash(c) & 8 = 8;
hash(d) = 59;  hash(d) & 7 = 3;hash(d) & 8 = 8;

假如此时HashMap的cap为 8,某个桶中已经形成链表,则可得到: table[3]=a->b->c->d。


如果此时扩容,将newCap设为16,我们可以看到如下结果:


hash(a) = 3; hash(a) & 15 = 3;
hash(b) = 11; hash(b) & 15 = 11;
hash(c) = 27; hash(c) & 15 = 11;
hash(d) = 59; hash(d) & 15 = 11;

我们会发现,当hash(k) & oldCap = 0 (即hash(a) = 3;的这个记录)时,这些链表的节点还是在原来的节点中(扩容后他的结果还是3) ,同时如果hash(k) & oldCap != 0时(11 27 59这几条记录),这些链表的节点会到桶中的其他的位置中 (从3变成了11)。


所以,对于链表来说,我们就不用逐人节点重新映射,而是直接通过hash(k) & ldCap进行分类,之后统一移动他们的位置即可。源码如下:


Node<K,V> loHead = null,loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
	next = e.next;
	if ((e.hash & oldCap) == 0) {
		if (loTail == null) {
			loHead = e;
		} else {
			loTail.next = e;
		}
		loTail = e;
	} else {
		if (hiTail == null) {
			hiHead = e;
		} else {
			hiTail.next = e;
		}
		hiTail = e;
	}
} while((e = next) != null);
if (loTail != null) {
	loTail.next = null;
	newTab[j] = loHead;
}

if (hiTail != null)  {
	hiTail.next = null;
	newTab[j + oldCap] = hiHead;
}

✔️ 取消树化


有了上面链表重新连接的经验,我们会发现,其实树化后的节点,也可以使用该操作来降低红黑树每人节点rehash时的时间复杂度,所以红黑树的 TreeNode 继承了链表的Node类,有了next字段,这样就可以像链表一样重新链接,源码如下:


TreeNode<K,V> loHead = null,loTail= null;
TreeNode<K,V> hiHead = null, hiTail = null;
for (TreeNode<K,V> e = b, next; e != null; e = next)  {
	next = (TreeNode<K,V>)e.next;
	e.next = null;
	if ((e.hash & bit) == 0) {
		if ((e.prev = loTail) == null)
			loHead = e;
		else 
			loTail.next = e;
		loTail = e;
		++lc;
	} else {
		if ((e.prev = hiTail) == null)
			hiHead = e;
		else
			hiTail.next = e;
		hiTail = e;
		++hc;
	}
}

当上面的操作完结后,HashMap会检测两个链表的长度,当元素小于等于6的时候,就会执行取消树化的操作,否则就会将新生成的链表重新树化。


取消树化非常简单,因为之前已经是条链表了,所以只需要将里面的元素由TreeNode转为Node即可。


至于重新树化的过程,请听下回分解~


✔️拓展知识仓


✔️除了rehash之外,哪些操作也会将树会退化成链表?


remove 元素的时候,这个过程中也会做退化的判断,如以下代码中,也会在这个分支中执行退化的操作(untreeify),如下代码所示:


if (root == null (movable 8& (root.right == null  (rl = root.left) == null rl.left == null))) {
	tab[index] = first.untreeify(map); // too small
	return;
}

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

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

相关文章

MySQL5.7 InnoDB 内存结构

官网地址&#xff1a;MySQL :: MySQL 5.7 Reference Manual :: 14.5 InnoDB In-Memory Structures 欢迎关注留言&#xff0c;我是收集整理小能手&#xff0c;工具翻译&#xff0c;仅供参考&#xff0c;笔芯笔芯. MySQL 5.7 参考手册 / ... / 缓冲池 14.5.1 缓冲池 缓冲池是…

从零开始搭建企业级前端项目模板(vue3+vite+ts)

文章目录 主要内容一、vite脚手架工具初始化项目二、项目代码加入eslint校验和自动格式化2.1安装对应依赖插件2.2 配置script脚本&#xff0c;项目安装eslint配置2.3 安装完成后&#xff0c;后面启动项目还缺少一些依赖&#xff0c;提前按需安装好 三&#xff0c;修改eslintrc.…

Vue3+Pinia实现持久化动态主题切换

PC端主题切换大家都用过&#xff0c;下面用Vue3Pinia实现一下这个过程; 【源码地址】 1、准备工作 npm install pinia npm install pinia-plugin-persist2、基础配置 // main.js import { createApp } from vue import App from ./App.vue import bootstrap from "../bo…

关于无人机上层控制的PID算法的思考

一、前言 背景介绍&#xff1a;PID虽然出现了很多年&#xff0c;但是目前工业界还是把PID作为主流的控制算法&#xff08;尽管学术界有很多非常时尚的控制算法&#xff0c;包括鲁邦控制&#xff0c;神经网络控制等等&#xff09;&#xff0c;PID的算法在于其不需要对系统进行复…

跟着小德学C++之安全模块

嗨&#xff0c;大家好&#xff0c;我是出生在达纳苏斯的一名德鲁伊&#xff0c;我是要立志成为海贼王&#xff0c;啊不&#xff0c;是立志成为科学家的德鲁伊。最近&#xff0c;我发现我们所处的世界是一个虚拟的世界&#xff0c;并由此开始&#xff0c;我展开了对我们这个世界…

Spring Security 6.x 系列(15)—— 会话管理之源码分析

一、前言 在上篇 Spring Security 6.x 系列(13)—— 会话管理之会话概念及常用配置 Spring Security 6.x 系列(14)—— 会话管理之会话固定攻击防护及Session共享 中了清晰了协议和会话的概念、对 Spring Security 中的常用会话配置进行了说明,并了解会话固定攻击防护…

WorkPlus完备的企业级功能堆栈,打造高效的企业移动平台

在如今的数字化时代&#xff0c;企业需要一个完备的功能堆栈来满足复杂的业务需求。WorkPlus作为一个完整的企业级移动平台&#xff0c;拥有完备的企业级功能&#xff0c;如IM、通讯录、内部群、模板群、工作台、权限管控、应用中心、日程管理、邮箱、同事圈、服务号、智能表单…

【Docker-Dev】Mac M2 搭建docker的redis环境

Redis的dev环境docker搭建 1、前言2、官方文档重点信息提取2.1、创建redis实例2.2、使用自己的redis.conf文件。 3、单机版redis搭建4、redis集群版4.1、一些验证4.2、一些问题 结语 1、前言 本文主要针对M2下&#xff0c;相应进行开发环境搭建&#xff0c;然后做一个文档记录…

FreeRTOS学习第6篇–任务状态挂起恢复删除等操作

目录 FreeRTOS学习第6篇--任务状态挂起恢复删除等操作任务的状态设计实验IRReceiver_Task任务相关代码片段实验现象本文中使用的测试工程 FreeRTOS学习第6篇–任务状态挂起恢复删除等操作 本文目标&#xff1a;学习与使用FreeRTOS中的几项操作&#xff0c;有挂起恢复删除等操作…

自动驾驶apollo9.0 Dreamview Debug方法

Apollo 9.0 安装&编译方法 # 拉取源码 git clone gitgithub.com:ApolloAuto/apollo.git git checkout v9.0.0# 启动docker bash docker/scripts/dev_start.sh bash docker/scripts/dev_into.sh# 编译project ./apollo.sh build默认启动方式 default mode wget https:…

MybatisPlus—自定义SQL

目录 1. 自定义SQL介绍 2. 自定义SQL使用步骤 3. 自定义SQL实例 4.总结 1. 自定义SQL介绍 介绍&#xff1a;自定义SQL并不是由我们来编写全部SQL语句&#xff0c;而是通过利用MyBatisPlus的Wrapper来构建复杂的Where条件&#xff0c;然后自己定义SQL语句中剩下的部分。 使…

详细全面的postman接口测试实战教程

基本介绍 postman是一款流程的接口调试工具&#xff0c;其特点就是使用简单&#xff0c;功能强大。使用角色也非常广泛&#xff0c;后端开发&#xff0c;前端人员&#xff0c;测试人员都可以使用它进行接口调试或测试。 基本框架 如果把postman去其内容只保留框架的话&#…

WorkPlus安全专属的即时通讯解决方案,助力企业高效沟通协作

在当今快节奏的商业环境中&#xff0c;高效的即时通讯是企业成功的关键。而WorkPlus作为一种领先的即时通讯工具&#xff0c;以其卓越的性能和创新的功能&#xff0c;助力企业高效沟通和协作。 WorkPlus作为即时通讯的新选择&#xff0c;为何备受企业的青睐&#xff1f;首先&am…

【JaveWeb教程】(7)Web前端基础:Vue组件库Element介绍与快速入门程序编写并运行 示例

目录 Element介绍快速入门示例 Element介绍 不知道同学们还否记得我们之前讲解的前端开发模式MVVM&#xff0c;我们之前学习的vue是侧重于VM开发的&#xff0c;主要用于数据绑定到视图的&#xff0c;那么接下来我们学习的ElementUI就是一款侧重于V开发的前端框架&#xff0c;主…

使用ChatGPT生成i项目需求文档模板

前言 我们在工作中需要编写的技术文档有多种形式&#xff0c;包括Word、Excel、PDF及一些在线形式。我们可以借助ChatGPT生成文本&#xff0c;然而&#xff0c;它不能直接生成Word、Excel、PDF等格式的文档。因此&#xff0c;我们需要利用其他工具来帮助我们生成一些模板&…

linux反汇编工具: ida pro、rizinorg/cutter; ubuntu 22 flameshot延迟截图 以应对下拉菜单

rizinorg/cutter rizinorg/cutter 是 命令行反汇编工具 rizinorg/rizin 的图形化界面, 这比 ida pro跑在kvm虚拟机中方便多了, ubuntu22.04下直接下载Cutter-v2.3.2-Linux-x86_64.AppImage后即可运行,如下图: 注意 有个同名的报废品: radare2/Cutter 即 radare2的图形化界…

软件测试|Linux三剑客之grep命令详解

简介 grep是一款在 Linux 和类 Unix 系统中广泛使用的文本搜索工具。它的名字来源于 Global Regular Expression Print&#xff08;全局正则表达式打印&#xff09;&#xff0c;它的主要功能是根据指定的模式&#xff08;正则表达式&#xff09;在文本文件中搜索并打印匹配的行…

JavaScript异常处理实战

前言 之前在对公司的前端代码脚本错误进行排查&#xff0c;试图降低 JS Error 的错误量&#xff0c;结合自己之前的经验对这方面内容进行了实践并总结&#xff0c;下面就此谈谈我对前端代码异常监控的一些见解。 本文大致围绕下面几点展开讨论&#xff1a; JS 处理异常的方式…

抖音在线查权重系统源码,附带查询接口

抖音权重在线查询只需输入抖音主页链接&#xff0c;即可查询作品情况。 搭建教程 上传源码并解压 修改数据库“bygoukai.sql” 修改“config.php” 如需修改水印请修改第40行 如需修改限制次数&#xff0c;请修改第156行 访问域名user.php即可查看访问用户&#xff0c;停…

学习笔记——C++运算符之赋值运算符

上次我们说到C的运算符共有四种&#xff0c;分别是算术运算符&#xff0c;赋值运算符&#xff0c;比较运算符和逻辑运算符 &#xff0c;下面介绍赋值运算符&#xff0c;赋值运算符主要的种类及作用如下表所示。 #include<bits/stdc.h> using namespace std; int main(){…