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

news2024/12/28 17:57:28

在这里插入图片描述

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方法省略

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + username.hashCode();
        result = 31 * result + email.hashCode();
        result = 31 * result + age;
        return result;
    }

    @Override
    public 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;
		// 临时的 key
		K 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的值赋值给 e
			e =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.coloradmin.cn/o/1367863.html

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

相关文章

MySql01:初识

1.mysql数据库2.配置环境变量3. 列的类型和属性&#xff0c;索引&#xff0c;注释3.1 类型3.2 属性3.3 主键(主键索引)3.4 注释 4.结构化查询语句分类&#xff1a;5.列类型--表列类型设置 1.mysql数据库 数据库&#xff1a; ​ 数据仓库&#xff0c;存储数据&#xff0c;以前我…

物理实验2023年下B卷部分题目总结

物理实验考试每个实验的题目由5个题变成8个题了QAQ 交直流电桥 1.惠斯通电桥不适于阻值较低&#xff08;1欧以下&#xff09;电阻的原因 2.立式电桥与卧式电桥的比较&#xff08;灵敏度、准确度、测量范围&#xff09; 3.交流电桥平衡法测电容的电路接线 4.铜热电阻、热敏…

Qt 6之五:创建菜单

Qt 6之五&#xff1a;创建菜单 Qt是一种跨平台的C应用程序开发框架&#xff0c;它提供了一套丰富的工具和库&#xff0c;可以帮助开发者快速构建跨平台的应用程序&#xff0c;用于开发图形用户界面&#xff08;GUI&#xff09;和非GUI应用程序。 Qt 6之一&#xff1a;简介、安…

初识大数据,一文掌握大数据必备知识文集(15)

&#x1f3c6;作者简介&#xff0c;普修罗双战士&#xff0c;一直追求不断学习和成长&#xff0c;在技术的道路上持续探索和实践。 &#x1f3c6;多年互联网行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f389;欢迎 &#x1f44d;点赞✍评论…

Kafka与RabbitMQ的区别

消息队列介绍 消息队列&#xff08;Message Queue&#xff09;是一种在分布式系统中进行异步通信的机制。它允许一个或多个生产者在发送消息时暂时将消息存储在队列中&#xff0c;然后由一个或多个消费者按顺序读取并处理这些消息。 消息队列具有以下特点&#xff1a; 异步通…

STM32F103C8T6(HAL库函数 - 内部Flash操作)

简介 STM32F103C8T6 内部Flash 为 64KB&#xff0c;本次将对他多余空间进行读写。 介绍 数据手册下载 STM32F103x8/STM32F103xB 数据手册 包含Flash Memory Page分布 STM32F设备命名 设备容量类型 中容量类型 内部空间介绍 64 KBytes大小Flash Memory 从 0x0800 0000 ~…

Mysql-排序查询方法

接上篇Mysql数据库的基础操作-CSDN博客 25. 基础-SQL-DCL-权限控制-_哔哩哔哩_bilibili 1、排序语法 2、查询结果示例 这个查询结果&#xff0c;因为特意选的age18 的数据来统计&#xff0c;所以当每一条数据的age一样时&#xff0c;使用worknno进行排序。可以看到work的升序和…

智能监控:业务监控新选择,效率提升新动力

前言 随着科技的飞速发展&#xff0c;企业对于业务的稳定性和连续性要求越来越高。传统的监控方式虽然在一定程度上能够保证业务的正常运行&#xff0c;但在面对复杂多变的业务场景和日益增长的数据量时&#xff0c;往往显得力不从心。为了解决这一问题&#xff0c;观测云在提…

NUXT3学习笔记

1.邂逅SPA、SSR 1.1 单页面应用程序 单页应用程序 (SPA) 全称是&#xff1a;Single-page application&#xff0c;SPA应用是在客户端呈现的&#xff08;术语称&#xff1a;CSR&#xff08;Client Side Render&#xff09;&#xff09; SPA的优点 只需加载一次 SPA应用程序只需…

迎接人工智能的下一个时代:ChatGPT的技术实现原理、行业实践以及商业变现途径

课程背景 2023年&#xff0c;以ChatGPT为代表的接近人类水平的对话机器人&#xff0c;AIGC不断刷爆网络&#xff0c;其强大的内容生成能力给人们带来了巨大的震撼。学术界和产业界也都形成共识&#xff1a;AIGC绝非昙花一现&#xff0c;其底层技术和产业生态已经形成了新的格局…

Linux-添加虚拟内存,不添加硬盘方式操作

在linux中&#xff0c;当物理内存mem不足时&#xff0c;就会使用虚拟内存(swap分区) 例如增加2G虚拟内存&#xff0c;操作如下: 1.查看内存大小 [rootlocalhost ~]# free -m 2.创建要作为swap分区的文件:增加1GB大小的交换分区&#xff0c;则命令写法如下&#xff0c;其中的cou…

探索区块链的未来:Ignis的母子架构进展与模块化区块链趋势

随着区块链技术的不断演进&#xff0c;模块化区块链成为热点&#xff0c;而其高拓展性的优点早在Ignis公链的母子架构上就已经实现。本文将探讨这两个方面&#xff0c;揭示它们如何推动区块链技术向前发展。 模块化区块链的兴起与Celestia 模块化区块链通过将不同的功能分解为…

开源C语言库Melon:Cron格式解析

本文介绍开源C语言库Melon的cron格式解析。 关于 Melon 库&#xff0c;这是一个开源的 C 语言库&#xff0c;它具有&#xff1a;开箱即用、无第三方依赖、安装部署简单、中英文文档齐全等优势。 Github repo 简介 cron也就是我们常说的Crontab中的时间格式&#xff0c;格式如…

一起来了解综合能源服务认证

首先&#xff0c;综合能源服务认证是有国家政策支持的&#xff0c; 《能源生产和消费革命战略&#xff08;2016-2030&#xff09;》中指出:1、能源生产端要以绿色低碳为方向&#xff0c;推动能源集中式和分布式开发并举&#xff0c;大幅提高新能源和可再生能源比重&#xff1b…

Async In C#5.0(async/await)学习笔记

此文为Async in C#5.0学习笔记 1、在async/await之前的异步 方式一&#xff1a;基于事件的异步Event-based Asynchronous Pattern (EAP). private void DumpWebPage(Uri uri) {WebClient webClient new WebClient();webClient.DownloadStringCompleted OnDownloadStringCo…

【大数据进阶第三阶段之ClickHouse学习笔记】ClickHouse的简介和使用

1、ClickHouse简介 ClickHouse是一种列式数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;专门用于高性能数据分析和数据仓库应用。它是一个开源的数据库系统&#xff0c;最初由俄罗斯搜索引擎公司Yandex开发&#xff0c;用于满足大规模数据分析和报告的需求。 开源地址…

01-shell

shell 1. shell概述 1.1 引入 完成以下任务: 判断用户家目录下&#xff08;~&#xff09;下面有没有一个叫 test 的文件夹如果没有&#xff0c;提示按 y 创建并进入此文件夹&#xff0c;按 n 退出如果有&#xff0c;直接进入&#xff0c;提示请输入一个字符串&#xff0c;并…

Qt/C++音视频开发63-设置视频旋转角度/支持0-90-180-270度旋转/自定义旋转角度

一、前言 设置旋转角度&#xff0c;相对来说是一个比较小众的需求&#xff0c;如果视频本身带了旋转角度&#xff0c;则解码播放的时候本身就会旋转到对应的角度显示&#xff0c;比如手机上拍摄的视频一般是旋转了90度的&#xff0c;如果该视频文件放到电脑上打开&#xff0c;…

探索PyTorch优化和剪枝技术相关的api函数

torch.nn子模块Utilities解析 clip_grad_norm_ torch.nn.utils.clip_grad_norm_ 是 PyTorch 深度学习框架中的一个函数&#xff0c;它主要用于控制神经网络训练过程中的梯度爆炸问题。这个函数通过裁剪梯度的范数来防止梯度过大&#xff0c;有助于稳定训练过程。 用途 防止…

数据权限-模型简要分析

权限管控可以通俗的理解为权力限制&#xff0c;即不同的人由于拥有不同权力&#xff0c;他所看到的、能使用的可能不一样。对应到一个应用系统&#xff0c;其实就是一个用户可能拥有不同的数据权限&#xff08;看到的&#xff09;和操作权限&#xff08;使用的&#xff09;。 …