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

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

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

相关文章

Profinet转Modbus网关助你畅享智能工业

Modbus转Profinet网关&#xff08;XD-MDPN100/200&#xff09;具有广泛的应用价值。无论是汽车制造、机械加工还是能源管理&#xff0c;都可以通过使用该网关&#xff0c;实现设备之间的高效通信。其次&#xff0c;Modbus转Profinet网关&#xff08;XD-MDPN100/200&#xff09;…

流行的Jmeter+Ant+Jenkins接口自动化测试框架在网络上走红

大致思路&#xff1a;Jmeter可以做接口测试&#xff0c;也能做压力测试&#xff0c;而且是开源软件&#xff1b;Ant是基于Java的构建工具&#xff0c;完成脚本执行并收集结果生成报告&#xff0c;可以跨平台&#xff0c;Jenkins是持续集成工具。将这三者结合起来可以搭建一套We…

初探ElasticSearch

1.什么是ElasticSearch&#xff1f; ElasticSearch简称ES&#xff0c;也成为弹性搜索&#xff0c;是基于Apache Lucene构建的开源搜索引擎。其实Lucene本身就是一款性能很好的开源搜索引擎工具包&#xff0c;但是Lucene的API相对复杂&#xff0c;而且掌握它需要很深厚的“内功…

人工智能_机器学习092_使用三维瑞士卷数据_利用分层聚类算法进行瑞士卷数据三维聚类---人工智能工作笔记0132

然后我们使用分层聚类算法来对我们导入的瑞士卷数据进行聚类 agg =AgglomerativeClustering(n_clusters = 6,linkage = ward) 可以看到这里我们使用的,聚类距离计算用的是,ward这种,最小化簇内方差的形式,l进行聚类对吧 可以看到这个linkage参数有好几个选择对吧,是之前我们讲过…

洛谷——P1983 [NOIP2013 普及组] 车站分级(拓扑排序、c++)

文章目录 一、题目[NOIP2013 普及组] 车站分级题目背景题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 提示 二、题解基本思路&#xff1a;代码 一、题目 [NOIP2013 普及组] 车站分级 题目背景 NOIP2013 普及组 T4 题目描述 一条单…

影响助听器使用寿命的因素

至少有九个因素会影响助听器的平均寿命&#xff1a; 用于制造助听器的材料清洁频率佩戴助听器的地方助听器的存放方式助听器样式一个人的身体生理学维护频率技术进步独特的听力需求 1.用于制作助听器的材料 虽然助听器的设计经久耐用&#xff0c;但由塑料、金属、硅、聚合物…

【经验】VSCode连接远程服务器(可以使用git管理、方便查看和编辑Linux源码)

1、查看OpenSSH Windows10通常自带OpenSSH不需要安装。 Windows10下检查是否已经安装OpenSSH的方法: 1)按下快捷键Win + X,选择Windows PoweShell(管理员) 2)输入以下指令: Get-WindowsCapability -Online | ? Name -like ‘OpenSSH*’ 3)如果电脑未安装OpenSSH,…

linux中最常用的文件操作命令

文章目录 Linux中最常用的文件操作命令查看庐山真面貌的cat实例 &#xff1a;简单显示内容实例 &#xff1a;显示行号 -n实例 &#xff1a; 显示行末实例&#xff1a;显示空白字符加一个管道 反向显示之 tac显示内容 且看迥然不同的 diff语法默认比较两个文件并排显示方便比较c…

1.4补码范围,溢出,补码加减法、加法器、竞争与冒险、杂项

正数三码合一 负数的原码有1的符号位&#xff0c;反码为除了符号位以外全部取反&#xff0c;补码在反码的基础上再加1 移码的符号位中0表示负数&#xff0c;1表示正数&#xff0c;简单来说&#xff0c;原码的补码数值位不变&#xff0c;符号位取反就是移码。 对于8位寄存器: …

从查询到高质量回答:发挥 RAG 和 Rerankers 的潜力

每日推荐一篇专注于解决实际问题的外文&#xff0c;精准翻译并深入解读其要点&#xff0c;助力读者培养实际问题解决和代码动手的能力。 欢迎关注公众号 原文标题&#xff1a;From Queries to Quality Answers: Harnessing the Potentials of RAG and Rerankers 原文地址&…

线性代数_对称矩阵

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

短说社区运营的使用工具分享(一)

本文是一篇针对短说社区运营的使用工具分享帖&#xff0c;是小编结合日常使用&#xff0c;总结的一些可以帮助网站管理员和运营人员进行日常操作和管理的工具。 1. 想天工作台之运营面板 想天工作台可以将桌面划分不同的类型来辅助办公&#xff0c;我分享下我当前的桌面情况&…