【Java集合篇】HashMap的put方法是如何实现的?

在这里插入图片描述

HashMap的put方法是如何实现的

  • ✔️典型解析
  • ✔️ 拓展知识仓
    • ✔️HashMap put方法的优缺点有哪些
    • ✔️如何避免HashMap put方法的哈希冲突
    • ✔️如何避免HashMap put方法的哈希重
  • ✔️源码解读
    • ✔️putVal 方法主要实现如下,为了更好的帮助大家阅读,提升效率,每一行都特意加了注释


✔️典型解析


先看一个简化的put方法实现,帮助理解其基本逻辑:


public V put(K key, V value) {  // 1. 计算键的哈希码  int hash = hash(key);  // 2. 计算桶的位置  int index = indexFor(hash, table.length);  // 3. 检查桶中是否有键值对  for (Entry<K,V> e = table[index]; e != null; e = e.next) {  Object k;  if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {  // 如果找到相同的键,则更新值  return e.setValue(value);  }  }  // 4. 如果桶为空或者找到了空位,插入新的键值对  addEntry(hash, key, value, index);  return null; // 表示新插入的键值对,其值是null  
}

下面是JDK 1.8中HashMap的put方法的简要实现过程:


1 . 首先,put方法会计算键的哈希值(通过调用hash方法),并通过哈希值计算出在数组中的索引位置。


2 . 如果该位置上的元素为空,那么直接将键值对存储在该位置上.


3 . 如果该位置上的元素不为空,那么遍历该位置上的元素,如果找到了与当前键相等的键值对,那么将该键值对的值更新为当前值,并返回旧值。


4 . 如果该位置上的元素不为空,但没有与当前键相等的键值对,那么将键值对插入到链表或红黑树中(如果该位置上的元素数量超过了一个闻值,就会将链表转化为红黑树来提高效率)。


5 . 如果插入成功,返回被替换的值:如果插入失败,返回null.


6 . 插入成功后,如果需要扩容,那么就进行一次扩容操作。


✔️ 拓展知识仓


✔️HashMap put方法的优缺点有哪些


HashMap的put方法在实现上具有一些优点和缺点。以下是一些可能的优缺点:

优点:


  1. 高效性:HashMap使用哈希表作为其底层实现,使得插入、删除和查找操作的时间复杂度接近O(1)。这使得HashMap在处理大量数据时具有很高的效率。

  1. 易于使用:HashMap提供了简单的方法来存储和检索数据,如put和get方法。这些方法易于使用,并且可以快速地添加新数据或检索现有数据。
  2. 动态扩展:HashMap可以根据需要自动调整其大小,以适应不同数量的数据。这使得HashMap能够处理任意数量的数据,而无需手动管理内存。

缺点:

  1. 哈希冲突:如果两个键的哈希码相同,则会发生哈希冲突。虽然HashMap使用链表或红黑树来解决冲突,但哈希冲突可能导致性能下降,特别是在高负载情况下。

  1. 线程不安全:HashMap不是线程安全的。如果在多线程环境中使用HashMap,需要额外的同步机制来保证线程安全。这可能导致性能下降和代码复杂度增加。

  1. 内存消耗:HashMap使用内存来存储键值对和哈希表结构。如果存储大量数据,可能会占用大量内存。

以上是HashMap put方法的一些优缺点。在实际应用中,选择合适的数据结构非常重要,需要根据具体需求进行权衡和选择。


✔️如何避免HashMap put方法的哈希冲突


哈希冲突是HashMap实现中不可避免的问题,但是可以通过一些策略来减少其影响:


  1. 选择合适的哈希函数:哈希函数是将键映射到桶的函数,选择一个好的哈希函数可以减少哈希冲突的发生。尽可能选择能够均匀分布键的哈希函数,以减少冲突的可能性。

  1. 使用链表或红黑树:当发生哈希冲突时,HashMap可以使用链表或红黑树来存储具有相同哈希码的键值对。链表可以存储多个键值对,而红黑树则可以平衡树中的节点,提高查找效率。

  1. 调整数组大小:如果哈希冲突过多,可以考虑增加HashMap的数组大小。这将重新分配内存,并可能导致性能下降。因此,应该谨慎地调整数组大小,以平衡性能和内存使用。

  1. 使用开放地址法:开放地址法是一种解决哈希冲突的方法,其中当发生冲突时,会在哈希表中寻找下一个可用的位置来存储键值对。这种方法可以减少哈希冲突的发生,但也可能导致性能下降。

  1. 合理设置负载因子:负载因子是已存储的键值对数量与桶的数量之比。如果负载因子过高,可能会导致哈希冲突增多。合理设置负载因子可以平衡性能和内存使用。

避免HashMap put方法的哈希冲突需要选择合适的哈希函数、使用链表或红黑树、调整数组大小、使用开放地址法和合理设置负载因子等方法。在实际应用中,需要根据具体情况进行权衡和选择。


✔️如何避免HashMap put方法的哈希重


避免HashMap的哈希冲突的关键在于设计一个好的哈希函数,使得键的哈希码尽可能均匀分布,减少冲突的可能性。以下是一些避免哈希冲突的策略:

  1. 选择合适的哈希函数:尽可能选择能够均匀分布键的哈希函数。哈希函数的设计应该考虑键的特性,使得不同的键能够产生不同的哈希码。

  2. 使用链表或红黑树:当发生哈希冲突时,HashMap可以使用链表或红黑树来存储具有相同哈希码的键值对。链表可以存储多个键值对,而红黑树则可以平衡树中的节点,提高查找效率。

  3. 调整数组大小:如果哈希冲突过多,可以考虑增加HashMap的数组大小。这将重新分配内存,并可能导致性能下降。因此,应该谨慎地调整数组大小,以平衡性能和内存使用。


  1. 合理设置负载因子:负载因子是已存储的键值对数量与桶的数量之比。如果负载因子过高,可能会导致哈希冲突增多。合理设置负载因子可以平衡性能和内存使用。

  1. 使用再哈希策略:当发生哈希冲突时,可以考虑使用再哈希策略。即当发生冲突时,使用另一个哈希函数计算新的哈希码,并尝试在新的位置存储键值对。这种方法可以减少冲突的可能性,但也可能导致性能下降。


    综上所述,避免HashMap的哈希冲突需要选择合适的哈希函数、使用链表或红黑树、调整数组大小、合理设置负载因子和使用再哈希策略等方法。在实际应用中,需要根据具体情况进行权衡和选择。

看一个Demo:

/**
*   使用HashMap存储用户信息,并避免哈希冲突
*/import java.util.HashMap;public class UserInfo {private String username;private String email;private int age;public UserInfo(String username, String email, int age) {this.username = username;this.email = email;this.age = age;}// getter和setter方法省略@Overridepublic int hashCode() {int result = 17;result = 31 * result + username.hashCode();result = 31 * result + email.hashCode();result = 31 * result + age;return result;}@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;UserInfo other = (UserInfo) obj;return age == other.age && username.equals(other.username) && email.equals(other.email);}
}

Demo中,定义了一个UserInfo类,用于存储用户信息。该类具有username、email和age属性,并实现了hashCode和equals方法。hashCode方法根据username、email和age计算哈希码,以确保不同的UserInfo对象具有不同的哈希码。equals方法用于比较两个UserInfo对象是否相等。

接下来,创建一个HashMap对象,用于存储UserInfo对象:

import java.util.HashMap;
import java.util.Map;public class UserDatabase {private Map<String, UserInfo> database;public UserDatabase() {database = new HashMap<>();}public void addUser(UserInfo user) {database.put(user.getUsername(), user);}public UserInfo getUser(String username) {return database.get(username);}
}

在Demo中,我们创建了一个UserDatabase类,用于管理用户信息。该类使用Map<String, UserInfo>作为底层实现,其中键是用户名,值是UserInfo对象。addUser方法用于添加用户信息,getUser方法用于根据用户名检索用户信息。注意,我们使用了UserInfo对象的username属性作为键,以确保不同的用户具有不同的键。这样,即使两个UserInfo对象的email和age属性相同,但由于键不同,它们仍然会被视为不同的键值对。这样可以减少哈希冲突的可能性。


✔️源码解读


put方法的代码很简单,就一行代码:


public V put(K key,V value) {return putVal(hash(key), key,value, false, true);
}

核心其实是通过 putValue 方法实现的,在传给 putValue 的参数中,先调用 hash 获取了一下hashCode


【Java集合篇】HashMap的hash方法是如何实现的?

✔️putVal 方法主要实现如下,为了更好的帮助大家阅读,提升效率,每一行都特意加了注释


/**
* Implements Map.put and related methods.
*   
* @param hash          key 的 hash 值
*  @param key          key 值
*  @param value        value 值
*  @param onlyIfAbsent true: 如果某个 key 已经存在那么就不插了; false 存在则替换,没有则新增。这里为 false
* @param evict         不用管了,我特喵也不认识
*  @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// tab 表示当前 hash 散列表的引用Node<KV>[] tab;//表示具体的散列表中的元素Node<k,V> p;//n:表示散列表数组的长度//i:表示路由寻址的结果int n, i;// 将 table 赋值发给 tab ;如果 tab == nul,说明 table 还没有被初始化。则此时是需要去创建 table 的//为什么这个时候才创建散列表因为可能创建了 HashMap 时候可能并没有存放数据,如果在初始化 ahMap 的时候就创建散列表,势必会造成空间的浪费//这里也就是延迟初始化的逻辑if ((tab = table) == null  (n = tab.length) == 0)  {n = (tab = resize()).length;}//如p == null说明寻址到的桶的位置没有元素。那么就将 key-value 封装到 Node 中,并放到寻址到的下标为的位置if ((p = tabli = (n - 1) & hash]) == null) {tab[i] = newNode(hash, key, value, null);}//到这里说明 该位置已经有数据了,且此时可能是链表结构,也可能是树结构 else {// e 表示找到了一个与当前要插入的key value 一致的元素Node<k,V> e;// 临时的 keyK k;//p 的值就是上一步 f 中的结果即: 此时的 (p = tab[i = (n - 1) & hash]) 不等于 null// p 是原来的已经在  位置的元素,且新插入的 key 是等于 p中的key// 说明找到了和当前需要插入的元素相同的元素(其实就是需要替换而已)if (p.hash == hash && ((k = p.key) == key  (key != null && key.equals(k))))//将 p的值赋值给 ee =p;//说明已经树化,红黑树会有单独的文章介绍,本文不再整述else if ((p instanceof TreeNode) {e = ((Treelode<KV>) p).putTreeVal(this, tab, hash, key, value);} else {//到这里说明不是树结构,也不相等,那说明不是同一个元素,那就是链表了for (int binCount = 0; : ++binCount)  {//如果 p.next == nul1 说明 p 是最后一个元素,说明,该元素在链表中也没有重复的,那么就需要添加到链表的if ((e = p.next) == null)  {//直接将 key-value 封装到 Node 中并且添加到 p的后面p.next = newNode(hash , key, value, null);//当元素已经是 7了,再来一个就是 8 个了,那么就需要进行树化if (binCount >= TREEIFY_THRESHOLD - 1) {treeifyBin(tab, hash);}break;}//在链表中找到了某个和当前元素一样的元素,即需要做替换操作了。if (e.hash == hash && ((k = e.key) == key  (key != null && key.equals(k))))  {break:}//将e(即p.next)赋值为,这就是为了继续遍历链表的下一个元素(没啥好说的)下面有张图帮助大家理解p= e;}}//如果条件成立,说明找到了需要替换的数据,if (e != null)  {//这里不就是使用新的值赋值为旧的值嘛V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)  {e.value = value;}//这个方法没用,里面啥也没有afterNodeAccess(e)://HashMap put 方法的返回值是原来位置的元素值return oldValue;}}/***    上面说过,对于散列表的 结构修改次数,那么就修改 modCount 的次数*/++ modCount;//size 即散列表中的元素的个数,添加后需要自增,如果自增后的值大于扩容的阑值,那么就触发扩容操作if (++size > threshold) {resize();}//啥也没干afterNodeInsertion(evict);//原来位置没有值,那么就返回 nul1 呗return null;
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++力扣题目--94,144,145二叉树非递归(迭代)遍历

为什么可以用迭代法&#xff08;非递归的方式&#xff09;来实现二叉树的前后中序遍历呢&#xff1f; 我们在栈与队列&#xff1a;匹配问题都是栈的强项 (opens new window)中提到了&#xff0c;递归的实现就是&#xff1a;每一次递归调用都会把函数的局部变量、参数值和返回地…

04、Kafka ------ 各个功能的作用解释(Cluster、集群、Broker、位移主题、复制因子、领导者副本、主题)

目录 启动命令&#xff1a;CMAK的用法★ 在CMAK中添加 Cluster★ 在CMAK中查看指定集群★ 在CMAK中查看 Broker★ 位移主题★ 复制因子★ 领导者副本和追随者副本★ 查看主题 启动命令&#xff1a; 1、启动 zookeeper 服务器端 小黑窗输入命令&#xff1a; zkServer 2、启动 …

1.1map

unordered_map和map的使用几乎是一致的&#xff0c;只是头文件和定义不同 #include<iostream> #include<map>//使用map需要的头文件 #include<unordered_map>//使用unordered_map需要的头文件 #include<set>//使用set需要的头文件 #include<uno…

【C#】网址不进行UrlEncode编码会存在一些问题

欢迎来到《小5讲堂》&#xff0c;大家好&#xff0c;我是全栈小5。 这是2024年第3篇文章&#xff0c;此篇文章是C#知识点实践序列文章&#xff0c;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 前言数据丢失效果请求端代码接口端代码…

2024--Django平台开发-Django知识点(四)

1.知识回顾 创建项目&#xff1a;新项目、别人项目、新版版、老版本 项目目录&#xff08;v1.0版本&#xff09; 路由系统 常见路由编写加粗样式 /index/ 函数 /index/<str:v1> 函数 re_path(ryy/(\d{4})-(\d{2})-(\d{2})/, views.yy), re_path(ryy/(?…

1.6PTA集练7-5~7-24、7-1、7-2,堆的操作,部落冲突(二分查找)

7-5 大師と仙人との奇遇 分数 20 #include<iostream> #include<queue> using namespace std; int n; long long ans0,num; priority_queue<long long,vector<long long>,greater<long long>>q;//记录之前买的,用小顶堆&#xff0c;最上面就是最…

用开源大语言模型开发的智能对话机器人初版原型验证

用开源大语言模型开发的智能对话机器人初版原型验证 0. 背景1. 初版检证效果展示2. 验证效果总结3. 20240108 更新 0. 背景 同事要想做一个智能对话机器人&#xff0c;特别的需求有有些几点&#xff0c; 通过预置提示词&#xff08;包括确认事项&#xff09;&#xff0c;让大…

【习题】应用程序框架

判断题 1. 一个应用只能有一个UIAbility。错误(False) 正确(True)错误(False) 2. 创建的Empty Ability模板工程&#xff0c;初始会生成一个UIAbility文件。正确(True) 正确(True)错误(False) 3. 每调用一次router.pushUrl()方法&#xff0c;页面路由栈数量均会加1。错误(Fal…

环信IM Demo登录方式如何修改为自己项目的?

在环信即时通讯云IM 官网下载Demo&#xff0c;本地运行只有手机验证码的方式登录&#xff1f;怎么更改为自己项目的Appkey和用户去进行登录呢&#xff1f; &#x1f447;&#x1f447;&#x1f447;本文以Web端为例&#xff0c;教大家如何更改代码来实现 1、 VUE2 Demo vue2…

自定义列表里面实现多选功能

需求 我们在开发过程中有时候会遇到列表里面会有多选&#xff0c;然后列表样式也要进行自定义。这里我们如果直接使用ElementUI组件el-table表格的时候这里实现起来可能比较复杂不方便&#xff0c;我们这里手写自定义一下列表里面多选的功能。 实现效果如下图所示&#xff1a…

云渲染适合什么场景下使用?

云渲染作为影视动画主流的渲染方案&#xff0c;通常云渲染服务商拥有专属的渲染农场&#xff0c;通过渲染农场庞大的高新能数量机器&#xff0c;可协助你在短时间内完成渲染任务。 云渲染使用场景有哪些&#xff1f; 1、硬件限制&#xff1a; 如果你的个人或公司电脑硬件不足…

Java内存模型(JMM)是基于多线程的吗

Java内存模型&#xff08;JMM&#xff09;是基于多线程的吗 这个问题按我的思路转换了下&#xff0c;其实就是在问&#xff1a;为什么需要Java内存模型 总结起来可以由几个角度来看待「可见性」、「有序性」和「原子性」 面试官&#xff1a;今天想跟你聊聊Java内存模型&#…

重新认识一下 vue3 应用实例

重新认识一下 vue 应用实例 &#x1f495; 创建应用实例 每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例 应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数&#xff0c;可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串 //…

【bug】【VSCode】远程终端TERMINAL打不开

【bug】【VSCode】远程终端TERMINAL打不开 可能的原因现象分析解决 可能的原因 昨天晚上vscode在打开多个TERMINAL的情况下&#xff0c;挂了一晚上&#xff0c;今早上来看的时候全都lost connections…。然后关闭再打开就出现了如上现象。 早上一来到实验室就要debug… 现象…

【UE Niagara学习笔记】03 - 火焰喷射效果

目录 效果 步骤 一、创建粒子系统 二、制作火焰动画 三、改为GPU粒子 四、循环播放粒子动画 五、火焰喷射效果雏形 六、火焰颜色 效果 步骤 一、创建粒子系统 1. 新建一个Niagara系统&#xff0c;选择模板 命名为“NS_Flame_Thrower”&#xff08;火焰喷射&#…

IntelliJ IDEA 如何编译 Maven 工程项目

在当今的Java开发领域&#xff0c;Maven已经成为项目构建和依赖管理的标准工具。IntelliJ IDEA作为一款集成度高的Java开发环境&#xff0c;提供了许多强大的功能来简化和优化Maven项目的构建流程。本文将深入介绍如何使用IntelliJ IDEA编译Maven工程的详细步骤以及一些高级技巧…

day6:进程间的通信

思维导图&#xff1a; 实现多个进程之间的收发信息操作 create.c&#xff1a; #include <head.h> int main(int argc, const char *argv[]) {if(mkfifo("a_send_b",0664)!0){perror("");return -1;}if(mkfifo("b_send_a",0664)!0){perro…

用Java编写图书网站信息采集程序教程

目录 一、准备工作 二、分析目标网站结构 三、选择信息采集方式 四、安装Jsoup库 五、编写信息采集程序 六、注意事项 总结&#xff1a; 编写图书网站信息采集程序需要掌握HTML、CSS、JavaScript、Java等前端和后端技术。下面是一个简单的教程&#xff0c;介绍如何使用…

Java后端开发——SSM整合实验

文章目录 Java后端开发——SSM整合实验一、常用方式整合SSM框架二、纯注解方式整合SSM框架 Java后端开发——SSM整合实验 一、常用方式整合SSM框架 1.搭建数据库环境&#xff1a;MySQL数据库中创建一个名称为ssm的数据库&#xff0c;在该数据库中创建一个名称为tb_book的表 …

Spring MVC(day1)

什么是MVC MVC是一种设计模式&#xff0c;将软件按照模型、视图、控制器来划分&#xff1a; M&#xff1a;Model&#xff0c;模型层&#xff0c;指工程中的JavaBean&#xff0c;作用是处理数据 JavaBean分为两类&#xff1a; 一类称为数据承载Bean&#xff1a;专门存储业务数据…