【Java集合篇】HashMap 在 get 和 put 时经过哪些步骤

news2024/11/16 20:57:27

在这里插入图片描述

HashMap在get和put时经过哪些步骤?

  • ✔️ 典型解析
  • ✔️get方法
  • ✔️put方法
  • ✔️ 拓展知识仓
    • ✔️ HashMap如何定位key
    • ✔️ HashMap定位tablelndex的骚操作作
    • ✔️HashMap的key为null时,没有hashCode是如何存储的?
    • ✔️ HashMap的value可以为null吗? 有什么优缺点讷?


✔️ 典型解析


对于HashMap来说,底层是基于散列算法实现,散列算法分为散列再探测拉链式HashMap 则使用了拉链式的散列算法,即采用数组+链表/红黑树来解决hash冲突,数组是HashMap的主体,链表主要用来解决哈希冲突。这个数组是Entry类型,它是HashMap的内部类,每一个Entry包含一个keyvalue键值对。


✔️get方法


对于get方法来说,会先查找桶,如果hash值相同并且key值相同,则返回该node节点,如果不同,则当node.next!=null时,判断是红黑树还是链表,之后根据相应方法进行查找。


直接看一个Demo吧,帮助理解。


import java.util.HashMap;  
import java.util.Map;  

// 定义一个HashMap类,该类继承了HashMap类 
public class ComplexHashMap<K, V> extends HashMap<K, V> {  
	  // 定义默认的初始容量和加载因子  
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
      
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
     // 定义树化操作的阈值   
    private static final int MAX_TREEIFY_THRESHOLD = 8;
      // 定义存储红黑树根节点的数组    
    private Entry<K, V>[] treeRoots;
    // 定义树化操作的阈值  
    private int treeifyThreshold;  
  
    public ComplexHashMap() {  
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  
    }  
  
    public ComplexHashMap(int initialCapacity, float loadFactor) {
    	// 调用父类的构造函数进行初始化,传入初始容量和加载因子参数   
        super(initialCapacity, loadFactor);  
        treeRoots = new Entry[DEFAULT_INITIAL_CAPACITY];
         // 计算树化操作的阈值,该值等于初始容量乘以加载因子    
        treeifyThreshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
    }  
  	
  	// 重写父类的rehash方法,用于在需要时重新哈希键值对并可能进行树化操作  
    @Override  
    protected void rehash(int newCapacity) {  
        super.rehash(newCapacity);  
        if (newCapacity > treeifyThreshold && size > MAX_TREEIFY_THRESHOLD) {  
            for (Entry<K, V> entry : this.entrySet()) {  
                K key = entry.getKey();  
                if (containsKey(key)) {  
                    put(key, entry.getValue());  
                }  
            }  
            treeify();  
        }  
    }  
  	// 定义一个私有方法treeify用于将map中的元素根据键进行排序并重新组织成红黑树结构以提升查询效率。
    private void treeify() {  
        Entry<K, V>[] newTreeRoots = new Entry[size()];  
        for (Entry<K, V> entry : this.entrySet()) {  
            K key = entry.getKey();  
            int index = Math.abs(key.hashCode()) % newTreeRoots.length;  
            if (newTreeRoots[index] == null) {  
                newTreeRoots[index] = new Entry<>(key, entry.getValue());  
            } else {  
                Entry<K, V> current = newTreeRoots[index];  
                while (true) {  
                    int comparison = current.key.compareTo(key);  
                    if (comparison == 0) {  
                        current.value = entry.getValue(); // replace value if different key with same hashcode found  
                        break;  
                    } else if (comparison < 0) {  
                        if (current.left == null) {  
                            current.left = new Entry<>(key, entry.getValue());  
                            break;  
                        } else {  
                            current = current.left;  
                        }  
                    } else { // comparison > 0  
                        if (current.right == null) {  
                            current.right = new Entry<>(key, entry.getValue());  
                            break;  
                        } else {  
                            current = current.right;  
                        }  
                    }  
                } 
            }   
        } 
        treeRoots = newTreeRoots; 
    }    
} 

✔️put方法


对于put方法来说,一般经过以下几步:


1 . 如果数组没有被初始化,先初始化数组


2 . 首先通过定位到要 putkey 在哪个桶中,如果该桶中没有元素,则将该要 putentry 放置在该桶中


3 . 如果该桶中已经有元素,则遍历该桶所属的链表:
    a . 如果该链表已经树化,则执行红黑树的插入流程


    b . 如果仍然是链表,则执行链表的插入流程,如果插入后链表的长度大于等于8,并目桶数组的容量大于等于64,则执行链表的树化流程


    c . 注意: 在上面的步骤中,如果元素和要put的元素相同,则直接替换


4 . 校验是新增 KV 还是替换老的KV,如果是后者,则设置 callback 扩展(LinkedHashMap LRU 即通过此实现)


5 . 校验 ++size 是否超过 threshold ,如果超过,则执行扩容流程 (见下会分解~)


读完文字,我们借助于代码片段捋一捋:


import java.util.HashMap;  
import java.util.Map;  

/**
*   @author xinbaobaba
*   一个简单的Demo,帮助理解HashMap在put操作时的基本步骤
*/  
public class HashMapPutExample {  
    public static void main(String[] args) {  
        // 创建一个新的HashMap对象  
        Map<String, Integer> map = new HashMap<>();  
  
        // 添加键值对到HashMap中  
        map.put("Alice", 25);  
        map.put("Bob", 30);  
        map.put("Charlie", 35);  
  
        // 输出原始HashMap的状态  
        System.out.println("Before modification:");  
        for (Map.Entry<String, Integer> entry : map.entrySet()) {  
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());  
        }  
  
        // 修改一个键对应的值  
        map.put("Alice", 30);  
  
        // 输出修改后的HashMap的状态  
        System.out.println("\nAfter modification:");  
        for (Map.Entry<String, Integer> entry : map.entrySet()) {  
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());  
        }  
    }  
}

输出结果如下:


Before modification:  
Key: Alice, Value: 25  
Key: Bob, Value: 30  
Key: Charlie, Value: 35  
  
After modification:  
Key: Alice, Value: 30  
Key: Bob, Value: 30  
Key: Charlie, Value: 35

✔️ 拓展知识仓


✔️ HashMap如何定位key


先通过 (table.length - 1) & (key.hashCode ^ (key.hashCode >> 16)) 定位到 key 位于哪个table 中,然后再通过key.equals(rowKey)来判断两个key是否相同,综上,是先通过hashCodeequals 来定位 KEY 的。


源码如下:


static final int hash(Object key) {
	int h;
	return (key == null) ?  0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
	// ...省略
	if ((p = tab[i = (n - 1) & hash]) == null) {
		tab[i] = newNode(hash, key, value, null);
	} else {
		Node<K,V> e; K k;
		// 这里会通过equals判断
		if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))  {
			e = p;
		} else if (p instanceof TreeNode) {
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		}
	// ...省略
	return null:
	}
}

所以,在使用 HashMap 的时候,尽量用StringEnum 等已经实现过 hashCodeequals方法的官方库类,如果一定要自己的类,就一定要实现 hashCodeequals 方法。


✔️ HashMap定位tablelndex的骚操作作


通过源码发现,hashMap 定位 tablelndex 的时候,是通过 (table.length - 1)& (key.hashCode ^ (key.hashCode >> 16)) ,而不是常规的key.hashCode % (table.length)呢?


1 . 为什么是用 & 而不是用 % :

:因为 & 是基于内存的二进制直接运算,比转成十进制的取模快的多。以下运算等价: X % 2^n = X & (2^n - 1) 。这也是 hashMap 每次扩容都要到2^n的原因之一

2 . 为什么用 key.hash ^ (key.hash >> 16)而不是用key.hash:

:这是因为增加了扰动计算,使得 hash分布的尽可能均匀。因为 hashCodeint 类型,虽然能映射40亿左右的空间,但是,HashMaptable.length毕竟不可能有那么大,所以为了使 hash%table.length 之后,分布的尽可能均匀,就需要对实例的hashCode的值进行扰动,说白了,就是将hashCode的高16和低16位,进行异或使得hashCode的值更加分散一点


✔️HashMap的key为null时,没有hashCode是如何存储的?


HashMap 对 key=null 的 case 做了特殊的处理,key值为 null 的 kv 对,总是会放在数组的第一个元素中,如下源码所示:


private V putForNulKey(V value) {
	for (Entry<K,V> e = table[0]; e != null; e = e.next)  {
		if (e.key == null) {
			V oldValue = e.value;
			e.value = value;
			e.recordAccess(this);
			return oldValue;
		}
	}
	
	modCount++;
	addEntry(0,null, value, 0);
	return null;
}


private V getForNul1Key()  {
	for (Entry<K,V> e = table[0]; e != null; e = e.next)  {
		if (e.key == null)
			return e.value;
	}
	return null;
}

✔️ HashMap的value可以为null吗? 有什么优缺点讷?


HashMap的kevvalue都可以为null,优点很明显,不会因为调用者的粗心操作就抛出NPE这种RuntimeException,但是缺点也很隐蔽,就像下面的代码一样:


//调用远程RPC方法,获取map
Map<StringObject> map = remoteMethod.queryMap();
//如果包含对应key,则进行业务处理
if(map.contains(KEY)) {
	String value = (string)map.get(KEY);
	System.out.printIn(value );
}

虽然map.contains(key),但是 map.get(key)==null,就会导致后面的业务逻辑出现NPE问题。

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

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

相关文章

STM32通用定时器-输入捕获-脉冲计数

一、知识点 编码器   两相编码器&#xff08;正交编码器&#xff09;&#xff1a;两相编码器由 A 相和 B 相组成&#xff0c;相位差为 90 度。当旋转方向为顺时针时&#xff0c;A 相先变化&#xff0c;然后 B 相变化&#xff1b;当旋转方向为逆时针时&#xff0c;B 相先变化…

让人头痛事务问题到底要如何解决?

前言 正好前段时间我在公司处理过这个问题&#xff0c;我们当时由于项目初期时间比较紧张&#xff0c;为了快速完成业务功能&#xff0c;忽略了系统部分性能问题。项目顺利上线后&#xff0c;专门抽了一个迭代的时间去解决大事务问题&#xff0c;目前已经优化完成&#xff0c;并…

【Linux Shell】7. printf 命令

文章目录 【 1. printf 命令的使用方法 】【 2. 实例 】 【 1. printf 命令的使用方法 】 printf 命令模仿 C 程序库&#xff08;library&#xff09;里的 printf() 程序&#xff0c;printf 由 POSIX 标准所定义&#xff0c;因此使用 printf 的脚本比使用 echo 移植性好。prin…

基于引力搜索算法优化的Elman神经网络数据预测 - 附代码

基于引力搜索算法优化的Elman神经网络数据预测 - 附代码 文章目录 基于引力搜索算法优化的Elman神经网络数据预测 - 附代码1.Elman 神经网络结构2.Elman 神经用络学习过程3.电力负荷预测概述3.1 模型建立 4.基于引力搜索优化的Elman网络5.测试结果6.参考文献7.Matlab代码 摘要&…

互联网演进历程:从“全球等待”到“全球智慧”的技术革新与商业变革

文章目录 一、导言二、World Wide Wait (全球等待)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 三、World Wide Web (万维网)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 四、World Wide Wisdom (全球智慧)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 五、…

静态网页设计——电影角(HTML+CSS+JavaScript)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 使用技术&#xff1a;HTMLCSSJS 主要内容&#xff1a;本网页主要利用HTML语言编写&#xff0c;简要介绍世界上一些主要国家&#xff0c;例如&#xff0c;中&#xff0c;…

从马尔可夫奖励过程到马尔可夫决策到强化学习【02/2】

一、说明 随着 Open AI 于 2023 年 11 月 6 日发布GPT 代理&#xff0c;我们所有人都对它带来的支持和灵活性着迷。想象一下&#xff0c;有一个个性化的数字助手始终在您身边&#xff0c;根据您的喜好完成日常平凡任务或艰巨任务。但为这些定制代理提供动力的是强化学习&#x…

厚积薄发11年,鸿蒙究竟有多可怕

12月20日中国工程院等权威单位发布**《2023年全球十大工程成就》。本次发布的2023全球十大工程成就包括“鸿蒙操作系统”在内。入围的“全球十大工程成就”&#xff0c;主要指过去五年由世界各国工程科技工作者合作或单独完成且实践验证有效的&#xff0c;并且已经产生全球影响…

指针数组做main函数的形参

目录 ​编辑 1. 指针数组 1.1 基本概念 1.2 简单示例 2. 指针数组做main形参 2.1 int main(int argc, char *argv[]); 2.2 简单示例 1. 指针数组 1.1 基本概念 指针数组是指一个数组&#xff0c;其中的每个元素都是指针。 这意味着数组中的每个元素都存储一个地址&…

啊哈c语言——逻辑挑战8:验证哥德巴赫猜想

上面这封书信是普鲁士数学家哥德巴赫在1742年6月7日写给瑞士数学家欧拉的&#xff0c;哥德巴赫在书信中提出了“任一大于2的整数都可以写成3个质数之和”的猜想。当时&#xff0c;哥德巴赫遵照的是“1也是素数”的约定。现今&#xff0c;数学界已经不使用这个约定了。哥德巴赫原…

LLM增强LLM;通过预测上下文来提高文生图质量;Spikformer V2;同时执行刚性和非刚性编辑的通用图像编辑框架

文章首发于公众号&#xff1a;机器感知 LLM增强LLM&#xff1b;通过预测上下文来提高文生图质量&#xff1b;Spikformer V2&#xff1b;同时执行刚性和非刚性编辑的通用图像编辑框架 LLM Augmented LLMs: Expanding Capabilities through Composition 本文研究了如何高效地组…

vmware虚拟机安装esxi7.0步骤

一、安装准备 1、下载镜像文件 下载链接&#xff1a;https://pan.baidu.com/s/12XmWBCI1zgbpN4lewqYw6g 提取码&#xff1a;mdtx 2、vmware新建一个虚拟机 2.1 选择自定义 2.2 选择ESXi对应版本 2.3 选择稍后安装操作系统 2.4 默认选择 2.5 自定义虚拟机名称及存储位置 2…

手机与电脑投屏互联方案

手机 to 电脑 无线显示器 搜索"无线显示器"找到系统自带的应用 没有的话, 可能需要安装一下 电脑上打开无线显示器 手机中打开投屏 就投上去了, 感觉很卡, 不是很流畅,但是是系统自带的功能, 比较方便 无法连接时可以检查一下这里的设置 scrcpy screen copy 屏幕…

透明OLED屏制作:工艺与技术挑战

透明OLED屏作为一种前沿的显示技术&#xff0c;其制作过程涉及一系列复杂的工艺和技术挑战。作为一名专注于OLED技术研发的工程师&#xff0c;我将为大家深入解析透明OLED屏的制作过程&#xff0c;以及所面临的挑战。 首先&#xff0c;透明OLED屏的制作过程大致可分为以下几个步…

LabVIEW开发智能水泵监测系统

LabVIEW开发智能水泵监测系统 水泵作为水利、石化、农业等领域的重要设备&#xff0c;其能效与健康状态直接关系到提灌泵站的运行效率。尽管水泵机组在全球能源消耗中占有显著比例&#xff0c;但实际运行效率常因设备老化和维护不当而远低于预期。这一状况需要更高效的监测手段…

Proxmox VE 8 安装开源监控平台Centreon 23

作者&#xff1a;田逸&#xff08;formyz&#xff09; 非常好用的开源监控系统Centreon从版本号21.40以后&#xff08;包括Centreon 21.40这个版本&#xff09;&#xff0c;不在提供ISO一键式安装包&#xff0c;取而代之的是在线脚本安装和VMware虚拟机或者Oracle VirtualBox 虚…

1-并发编程线程基础

什么是线程 在讨论什么是线程前有必要先说下什么是进程&#xff0c;因为线程是进程中的一个实体&#xff0c;线程本身是不会独立存在的。 进程是代码在数据集合上的一次运行活动&#xff0c;是系统进行资源分配和调度的基本单位&#xff0c;线程则是进程的一个执行路径&#…

线性代数_对称矩阵

对称矩阵是线性代数中一种非常重要的矩阵结构&#xff0c;它具有许多独特的性质和应用。下面是对称矩阵的详细描述&#xff1a; ### 定义 对称矩阵&#xff0c;即对称方阵&#xff0c;是指一个n阶方阵A&#xff0c;其转置矩阵等于其本身&#xff0c;即A^T A。这意味着方阵A中的…

YOLOv8模型yaml结构图理解(逐层分析)

前言 YOLO-V8&#xff08;官网地址&#xff09;&#xff1a;https://github.com/ultralytics/ultralytics 一、yolov8配置yaml文件 YOLOv8的配置文件定义了模型的关键参数和结构&#xff0c;包括类别数、模型尺寸、骨架&#xff08;backbone&#xff09;和头部&#xff08;hea…

Linux安装JDK和Maven并配置环境变量

文章目录 一、安装JDK并配置环境变量二、安装maven并配置环境变量 一、安装JDK并配置环境变量 将JDK的安装包上传到Linux系统的usr/local目录 使用xftp上传文件 解压JDK的压缩包 xshell连接到云主机 [roottheo ~]# cd /usr/local[roottheo local]# ls aegis apache-tomcat-…