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

在这里插入图片描述

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问题。

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

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

相关文章

ZkSync第一Dex空投交互全教程,Holdstation ZK热点不容错过

2023 年 12 月 8 日&#xff0c;在以太坊基金会的 176 次会议上&#xff0c;开发人员一致同意&#xff0c;如果事情进展顺利&#xff0c;将在 2024 年初定 Goerli 分叉日期&#xff0c;目标是能在 2024 年 1 月激活 Goerli Dencun 测试网&#xff0c;预计能够在 2024 年 3 月~ …

SOFA Framework源代码及插件Win11编译开发环境配置

这篇文章主要记录详细的SOFA Framework软件的源代码编译环境配置过程&#xff0c;开发环境基于Win系统&#xff0c;编译完成后&#xff0c;可以在插件或框架的源代码上进行开发集成。本文纯手写输入&#xff0c;言简意赅&#xff0c;以大方向和思路为准&#xff0c;具体需要注意…

torch.meshgrid和np.meshgrid的区别

numpy中meshgrid&#xff1a; 把数组a当作一行&#xff0c;再根据数组b的长度扩充行。 把数组b当作一列&#xff0c;再根据数组a的长度扩充列。 torch中meshgrid&#xff1a; 把数组a当作一列&#xff0c;再根据数组b的长度扩充列。 把数组b当作一行&#xff0c;再根据数组a的…

前端 js 基础对象 (3)

js 对象定义 <!DOCTYPE html> <html> <body><h1>JavaScript 对象创建</h1><p id"demo1"></p> <p>new</p> <p id"demo"></p><script> // 创建对象&#xff1a; var persona {fi…

C++ 释放指针

在C中&#xff0c;释放指针通常使用delete或delete[]操作符&#xff1b; 如果指针指向的是单个对象&#xff0c;可以使用delete操作符进行释放&#xff1b; 在释放完内存后&#xff0c;最好将指针置为nullptr&#xff0c;以避免出现悬空指针&#xff08;dangling pointer&#…

LeGO-LOAM 几个特有函数的分析(2)

接上回LeGO-LOAM 几个特有函数的分析&#xff08;1&#xff09; 二、广度优先遍历 广度优先遍历&#xff08;Breadth-First Search, BFS&#xff09;是一种用于遍历或搜索树或图的算法。这种算法从树的根&#xff08;或图的某一指定节点&#xff09;开始&#xff0c;然后探索…

「小明赠书活动」2024第二期《实战AI大模型》

⭐️ 赠书 - 《实战AI大模型》 从基本概念到实践技巧的&#xff0c;全方位解读AI大模型&#xff0c;手把手教你训练和部署BERT、GPT-3、ChatGPT&#xff01; 人工智能领域资深专家尤洋老师倾力打造&#xff0c;获得了 李开复、周鸿祎、颜水成 三位大咖鼎力推荐&#xff0c;一经…

【前端】[vue3] vue-router使用

提示&#xff1a;我这边用的是typeScript语法&#xff0c;所以js文件的后缀都是ts。 安装vue-router&#xff1a; &#xff08;注意&#xff1a;vue2引用vue-router3 vue3才引用vue-router4&#xff09; npm install vue-router4src文件夹下面创建 router/index.ts&#xff08;…

安科瑞微机综合保护测控装置在某电厂10.5kV厂用电系统改造中的应用——安科瑞 顾烊宇

摘要&#xff1a;某电厂8号机10.5kV厂用电二次系统设备大多为常规电磁式继电器、电量变送器等。通过对厂用电二次系统从设备选型、设计、施工调试等方面进行的改造&#xff0c;尤其微机综合保护测控装置的应用&#xff0c;集控制、保护、测量、信号报警、开关量采集、通讯功能于…

Python+Appium自动化测试的使用步骤

这篇文章主要介绍了PythonAppium实现自动化测试的使用步骤&#xff0c;文中通过示例代码介绍的非常详细&#xff0c;对大家的学习或者工作具有一定的参考学习价值&#xff0c;需要的朋友们下面随着小编来一起学习学习吧 一、环境准备 1.脚本语言&#xff1a;Python3.x IDE&am…

IIS回收应用

前言 作为Windows的一个可选包,Internet Information Services (IIS)管理器经常被用于Windows Server系列服务器内的Web管理。IIS采用应用程序池方式管理Web的工作进程,同时采用了页面输出缓存的缓存加载机制。当网络出现瞬间访问异常时,部分IIS管理的web页面可能会发生长…

vue登录 滑动验证,记住密码及AES加密解密

相关依赖 npm install js-cookie //js-cookie npm install crypto-js //AES加密解密 npm install -S vue-puzzle-vcode //滑动验证 vue2 <template><div class"login"><div class"login-box"><div class"imgbox">&…

BOM介绍

文章目录 1、简介主要作用 2、BOM的组成2.1 窗口对象window2.1.1 window对象特点2.1.2 window作用域2.1.3 window对象常见方法**第一类&#xff1a;系统对话框**第二类&#xff1a;控制浏览器窗口方法第三类&#xff1a;与定时器有关的方法 1、简介 BOM&#xff08;Browser Ob…

面试算法98:路径的数目

题目 一个机器人从mn的格子的左上角出发&#xff0c;它每步要么向下要么向右&#xff0c;直到抵达格子的右下角。请计算机器人从左上角到达右下角的路径的数目。例如&#xff0c;如果格子的大小是33&#xff0c;那么机器人从左上角到达右下角有6条符合条件的不同路径。 分析…

教学/直播/会议触摸一体机定制_基于展锐T820安卓核心板方案

触控一体机是一种集先进的触摸屏、工控和计算机技术于一体的设备。它取代了传统的键盘鼠标输入功能&#xff0c;广泛应用于教学、培训、工业、会议、直播、高新科技展示等领域。触摸一体机的应用提升了教学、会议和展示的互动性和信息交流。 触摸一体机方案基于国产6nm旗舰芯片…

鸿蒙问题之本地模拟器无法识别

今天按例打开本地模拟器&#xff0c;发现DevEco Studio不能检测到我的本地模拟器了。 重启了DevEco Studio和模拟器多次都无果。果断删除模拟器 然后创建一个新的&#xff0c;就可以成功检测到了。这应该是idea的一个bug

外包干了5个月,技术明显退步了...

先说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入湖南某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年12月份&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测…

《知识扫盲》ROS和ROS2对比

文章摘选自&#xff1a;ROS与ROS2对比 1.ROS问题举例 ROS的设计目标是简化机器人的开发&#xff0c;如何简化呢&#xff1f;ROS为此设计了一整套通信机制&#xff08;话题、服务、参数、动作&#xff09;。 通过这些通信机制&#xff0c;ROS实现了将机器人的各个组件给的连接…

基于springboot的java读取文档内容(超简单)

读取一个word文档里面的内容&#xff0c;并取出来。 代码&#xff1a; SneakyThrowsGetMapping(value "/readWordDoc")ApiOperationSupport(order 1)ApiOperation(value "文档读取 ", notes "文档读取 ")public R ReadWordDoc () {System.o…

如何在 ChatGPT 上使用 Wolfram 插件回答数学问题

这里写自定义目录标题 写在最前面Wolfram是什么&#xff1f;ChatGPT 如何与 Wolfram 相结合&#xff0c;为什么有效&#xff1f;如何在 ChatGPT 上安装 Wolfram 插件&#xff1f; 写在最前面 参考&#xff1a;https://clickthis.blog/zh-CN/how-to-answer-math-questions-usin…