Java面试八股文宝典:初识数据结构-数组的应用扩展之HashMap

前言

除了基本的数组,还有其他高级的数据结构,用于更复杂的数据存储和检索需求。其中,HashMap 是 Java 集合框架中的一部分,用于存储键值对(key-value pairs)。HashMap 允许我们通过键来快速查找和检索值,类似于字典或关联数组的概念。HashMap 在实际编程中广泛应用于各种场景,包括缓存、数据库索引、数据存储等。

HashMapHashTable

HashMapHashTable 都是键值对存储的数据结构,它们可以用于快速查找和检索数据。虽然它们在用法上很相似,但也存在一些重要的区别:

  • HashMap HashMap 是 Java 集合框架中的一部分,它允许空键(key)和空值(value),并且是非线程安全的。HashMap 使用了哈希表的数据结构,能够在常数时间内查找元素。

  • HashTable HashTable 也是键值对存储的数据结构,但它不允许空键和空值,而且是线程安全的。HashTable 使用了类似于 HashMap 的哈希表实现,但在多线程环境下性能更好。

示例代码 - 使用 HashMap

让我们看一下如何使用 HashMap 存储和检索数据:

// 创建一个 HashMap
HashMap<String, Integer> scores = new HashMap<>();// 插入键值对
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 92);// 检索值
int aliceScore = scores.get("Alice"); // 获取 Alice 的成绩

HashMap底层代码解析

理解 HashMap 内部工作原理的关键部分是研究其核心方法,如 resizetreeifyBinputVal。以下是这些方法的简要源码示例和解释:

1. resize 方法

resize 方法用于对 HashMap 进行扩容。当键值对数量达到阈值时,调用 resize 方法进行扩容,通常将容量翻倍。

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table; // 保存旧的桶数组int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧容量int oldThr = threshold; // 旧的阈值int newCap, newThr = 0; // 新容量和新阈值if (oldCap > 0) { // 如果旧容量大于0if (oldCap >= MAXIMUM_CAPACITY) { // 如果旧容量已经达到最大值threshold = Integer.MAX_VALUE; // 阈值设置为最大值,禁止再次扩容return oldTab; // 返回旧的桶数组} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 扩容为原容量的两倍,并更新新阈值} else if (oldThr > 0) // 如果旧的阈值大于0newCap = oldThr; // 使用阈值作为新容量else { // 如果没有指定容量和阈值,使用默认值newCap = DEFAULT_INITIAL_CAPACITY; // 默认初始容量newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 默认阈值}if (newThr == 0) { // 如果新阈值为0float ft = (float)newCap * loadFactor; // 计算新阈值newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE); // 如果未超过最大容量,使用计算值,否则使用最大值}threshold = newThr; // 更新阈值@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新的桶数组table = newTab; // 将桶数组引用指向新的桶数组if (oldTab != null) { // 如果旧桶数组不为空for (int j = 0; j < oldCap; ++j) { // 遍历旧桶数组Node<K,V> e;if ((e = oldTab[j]) != null) { // 如果当前位置有节点oldTab[j] = null; // 清空旧桶位置if (e.next == null)newTab[e.hash & (newCap - 1)] = e; // 如果只有一个节点,直接放入新桶位置else if (e instanceof TreeNode) // 如果节点是红黑树节点((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 进行拆分else { // 如果是链表Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) { // 如果哈希码对旧容量求与为0if (loTail == null)loHead = e; // 放入低位链表头部elseloTail.next = e; // 放入低位链表尾部loTail = e; // 更新低位链表尾节点}else { // 否则放入高位链表if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead; // 更新旧桶位置为低位链表}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead; // 更新旧桶位置为高位链表}}}}}return newTab; // 返回新的桶数组
}
  • resize 方法首先计算新的容量和阈值。
  • 如果旧表(oldTab)不为空,它将遍历旧表的桶,根据哈希值重新分配节点到新表(newTab)中。
  • 如果节点的数量超过一定阈值,它可能会将链表转化为红黑树,以提高性能。

2. treeifyBin 方法

treeifyBin 方法用于将链表转换为红黑树,以提高查找性能。该方法通常在链表长度超过8时被调用。

final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize(); // 扩容else if ((e = tab[index = (n - 1) & hash]) != null) { // 获取当前位置的节点TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null); // 创建红黑树节点if (tl == null)hd = p; // 如果链表为空,将红黑树节点设置为头节点else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null) // 如果头节点不为空,将链表转化为红黑树hd.treeify(tab);}
}
  • treeifyBin 方法首先检查表的容量,如果小于 MIN_TREEIFY_CAPACITY,则会调用 resize 方法进行扩容。
  • 如果表的容量足够大,它将遍历桶中的链表,将每个节点替换为红黑树节点,并构建红黑树。

3. pubVal 方法

putVal 方法是 HashMap 中用于向哈希表中添加键值对的核心方法。

/*** Associates the specified value with the specified key in this map.* If the map previously contained a mapping for the key, the old* value is replaced.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; // 声明桶数组Node<K,V> p; // 声明当前节点int n, i; // n为桶数组的长度,i为计算出的槽位索引// 如果桶数组为空或长度为0,需要初始化//判断tab是不是为空,如果为空,则将容量进行初始化,也就是说,初始换操作不是在new HashMap()的时候进行的,而是在第一次put的时候进行的if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 计算槽位索引:初始化操作以后,根据当前key的哈希值算出最终命中到哪个桶上去,并且这个桶上如果没有元素的话,则直接new一个新节点放进去if ((p = tab[i = (n - 1) & hash]) == null) // 如果当前槽位为空tab[i] = newNode(hash, key, value, null); // 直接创建新节点并放入槽位else {Node<K,V> e; K k;// 先判断一下这个桶里的第一个Node元素的key是不是和即将要存的key值相同,如果相同,则把当前桶里第一个Node元素赋值给e,这个else的最下边进行了判断,如果e!=null就执行把新value进行替换的操作if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p; // 如果找到相同键的节点,将e指向该节点else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {// 如果遍历到链表尾部仍未找到相同键的节点,将新节点添加到链表尾部if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 如果链表长度达到阈值,将链表转化为红黑树if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break; // 如果找到相同键的节点,退出循环p = e;}}/*** 只要e不为空,说明要插入的key已经存在了,覆盖旧的value值,然后返回原来oldValue* 因为只是替换了旧的value值,并没有插入新的元素,所以不需要下边的扩容判断,直接* return掉*/if (e != null) { // 如果找到相同键的节点V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value; // 替换值afterNodeAccess(e);return oldValue; // 返回旧值}}++modCount;/*** 判断容量是否已经到了需要扩充的阈值了,如果到了,则进行扩充* 如果上一步已经判断key是存在的,只是替换了value值,并没有插入新的元素,所以不需要判断* 扩容,不会走这一步的*/if (++size > threshold)resize(); // 如果超过阈值,进行扩容afterNodeInsertion(evict);return null; // 返回null表示插入成功
}

putVal 方法用于将键值对放入HashMap中。此方法涵盖了查找、替换、插入和扩容等关键操作,是HashMap内部工作的核心部分。

注意事项

在使用 HashMap 时,有一些重要的注意事项和最佳实践,以确保正确性和性能。以下是一些关键的注意事项:

  1. 线程安全性HashMap 不是线程安全的数据结构,如果在多线程环境下使用,需要考虑采取适当的同步机制,或者使用线程安全的替代品,如 ConcurrentHashMap

  2. 键的不可变性HashMap 中的键应该是不可变的对象。如果键发生了变化,可能导致无法正常获取或删除值。

  3. 哈希冲突:哈希冲突是指不同的键映射到相同的哈希桶。为了处理冲突,HashMap 使用链表或红黑树。为了获得良好的性能,尽量避免大量哈希冲突,可以考虑使用良好的哈希函数或合适的数据分布。

  4. 哈希函数重写:如果自定义对象作为键,应该确保重写了 hashCode()equals() 方法,以确保正确的哈希和相等性比较。

  5. 初始化容量和负载因子:在创建 HashMap 时,可以指定初始容量和负载因子。根据预期的键值对数量,选择适当的初始容量可以提高性能。负载因子是用于触发扩容的阈值,通常选择合适的默认值即可。

  6. 遍历:在遍历 HashMap 时,尽量不要修改其结构(添加或删除键值对),否则可能会导致不确定的行为或异常。

  7. Null 键和 Null 值HashMap 允许键和值为 null。但要小心处理 null 键,以防止 NullPointerException

  8. 性能考虑HashMap 在查找操作上有很好的性能,但在插入和删除操作上可能会有较差的性能,特别是在存在大量哈希冲突时。在需要频繁插入和删除的场景中,可以考虑使用 LinkedHashMapConcurrentHashMap

  9. 扩容代价HashMap 在达到负载因子阈值时会自动进行扩容,这涉及到重新分配键值对到新的桶数组。频繁的扩容操作可能会影响性能,因此应根据应用的需求选择适当的初始容量和负载因子。

  10. equals 和 hashCode 方法:如果自定义对象作为键,确保正确实现了 equalshashCode 方法,以便正确地比较和查找键。

总的来说,HashMap 是一个非常有用的数据结构,但在使用时需要谨慎考虑上述注意事项,以确保其正确性和性能。根据应用的需求,还可以考虑使用其他实现了特定场景需求的 Map 接口的实现类,如 ConcurrentHashMapLinkedHashMap 等。

JDK1.7和1.8对比

HashMap 在 JDK 1.7 和 JDK 1.8 中都有存在,但在 JDK 1.8 中进行了一些重要的改进。以下是 JDK 1.7 和 JDK 1.8 中 HashMap 的主要区别:

  1. 数据结构

    • JDK 1.7:JDK 1.7 中的 HashMap 使用数组 + 链表的数据结构。具体说,它使用数组存储桶(buckets),每个桶存储一个链表。这意味着当多个键映射到同一个桶时,它们会在同一个链表上存储。

    • JDK 1.8:JDK 1.8 中的 HashMap 在链表长度达到一定阈值(8)时,将链表转化为红黑树。这一改进在处理大量键值对时提高了查找性能,因为红黑树的查找时间复杂度为 O(log n)。
      在这里插入图片描述

  2. 哈希冲突解决

    • JDK 1.7:JDK 1.7 使用链表来解决哈希冲突。当多个键映射到同一个桶时,它们会形成一个链表,需要遍历链表来查找。

    • JDK 1.8:JDK 1.8 在链表长度达到一定阈值时,会将链表转化为红黑树,这大大提高了处理长链表的性能。

  3. 并发性能

    • JDK 1.7:JDK 1.7 中的 HashMap 不是线程安全的,如果多个线程同时操作一个 HashMap,可能会导致数据不一致或死锁等问题。为了在多线程环境下使用 HashMap,需要自行添加同步机制。

    • JDK 1.8:JDK 1.8 中的 HashMap 在处理并发操作时进行了优化。它引入了更高效的锁机制,例如分段锁和 CAS 操作,以提高并发性能。此外,JDK 1.8 还引入了 ConcurrentHashMap 类,专门用于高并发环境。

  4. 迭代性能

    • JDK 1.7:JDK 1.7 中的 HashMap 在迭代时性能较差,因为即使没有哈希冲突,它也需要遍历整个桶数组,包括空桶。

    • JDK 1.8:JDK 1.8 中的 HashMap 在迭代时性能得到了提升,特别是在没有哈希冲突的情况下。这是由于它使用了更好的数据结构和算法来加速迭代。

  5. 空间利用

    • JDK 1.7:JDK 1.7 中的 HashMap 对空间的利用不是很高,因为桶的数量必须是 2 的幂次方,可能会导致浪费空间。

    • JDK 1.8:JDK 1.8 中的 HashMap 在一定程度上改进了空间利用,通过采用树结构来存储哈希冲突的键值对,减少了空间浪费。
      在这里插入图片描述

总的来说,JDK 1.8 中的 HashMap 在性能和并发性能上有重大改进,特别是在处理大量数据和高并发访问时表现更优越。因此,如果使用 Java 8 或更高版本,通常建议使用 JDK 1.8 中的 HashMap 实现。但要注意,如果需要在多线程环境中使用 HashMap,最好考虑使用 ConcurrentHashMap 或其他线程安全的数据结构。

结语:

本文已经提供了有关 HashMap 的深入信息,包括其底层代码和注意事项。下一章将继续探讨 HashTable,以便能够全面了解这两个重要的数据结构。

如果您有任何疑问、建议或更正,请随时都可以留言或私信作者,我们将非常乐意为您提供帮助并改进文章。期待在下一章中继续分享有价值的知识。

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

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

相关文章

004-Windows下开发环境搭建

Windows下开发环境搭建 文章目录 Windows下开发环境搭建项目介绍版本控制工具Git 与 SVNWindow下安装Git Qt 开发工具静态编译Qt环境安装 串口模拟器比较工具SQLite 数据库查看小工具预告 关键字&#xff1a; Qt、 Qml、 开发环境、 Windows、 C 项目介绍 欢迎来到我们的 …

数据库操作-DML/DQL

数据库操作-DML DML英文全称是Data Manipulation Language(数据操作语言)&#xff0c;用来对数据库中表的数据记录进行增、删、改操作。 添加数据&#xff08;INSERT&#xff09; 修改数据&#xff08;UPDATE&#xff09; 删除数据&#xff08;DELETE&#xff09; 增加(ins…

python 异常

1.捕获异常 2.密码爆破 3.

基于b/s架构搭建一个支持多路摄像头的实时处理系统 (3) -- Django 结合WebSocket 进行实时图像与消息的展示, 前后端交互与定时任务的重构

文章大纲 consumersself.scope参考文献与学习路径javascript html websocketpython 定时任务Django 实时通信后端 channelconsumers https://channels.readthedocs.io/en/stable/topics/consumers.htmlself.scope consumer在初始化时会接受链接的scope,类似于Django中的reque…

查看云桌面请求linux服务器网络快慢

要查看云桌面请求 Linux 服务器的网络快慢&#xff0c;您可以使用一些网络诊断工具和命令来评估连接的性能。以下是一些方法&#xff1a; 使用 ping 命令&#xff1a; ping 命令可以测试与目标服务器之间的网络连接。在终端中运行以下命令以测试服务器的响应时间&#xff1a; p…

【业务功能118】微服务-springcloud-springboot-Kubernetes集群-k8s集群-KubeSphere-OpenELB部署及应用

OpenELB部署及应用 一、OpenELB介绍 网址&#xff1a; openelb.io OpenELB 是一个开源的云原生负载均衡器实现&#xff0c;可以在基于裸金属服务器、边缘以及虚拟化的 Kubernetes 环境中使用 LoadBalancer 类型的 Service 对外暴露服务。OpenELB 项目最初由 KubeSphere 社区发…

用Python把csv文件批量修改编码为UTF-8格式并转为Excel格式

有时候用excel打开一个csv文件&#xff0c;中文全部显示乱码。然后手动用notepad打开&#xff0c;修改编码为utf-8并保存后&#xff0c;再用excel打开显示正常。 使用Python通过很少代码就能将上面过程自动化。 导入3个模块 import pandas as pd import os import chardetc…

Python 3 – 文件 readline() 方法

Python 3 – 文件 readline() 方法|极客笔记 # 打开文件 file open("example.txt", "r")# 读取文件中的一行数据 line file.readline() while line:# 移除行尾的换行符print(line.strip())# 读取文件中的下一行数据line file.readline()# 关闭文件 file…

【Seata】05 - Seata Saga 模式简单整理、Docker 部署 Nacos 单机(基于 Jpom)相关配置

文章目录 前言参考目录Saga 模式知识点简单整理1、适用场景、优缺点2、Saga 模式的使用3、可能出现的问题以及解决方法 Docker 部署 Nacos 单机&#xff08;基于 Jpom&#xff09;步骤 1&#xff1a;拉取镜像步骤 2&#xff1a;构建容器步骤 3&#xff1a;Nacos 设置 Seata 配置…

自动化测试工具slelnium的初体验

1.slelnium介绍 1.1 一个Web的自动化测试工具&#xff0c;最初是为网站自动化测试而开发的。 1.2 可以直接运行在浏览器上&#xff0c;它支持所有主流的浏览器&#xff08;包括PhantomJS这些无界面的浏览器&#xff09;&#xff0c;可以接收指令&#xff0c;让浏览器自动加载页…

代码随想录Day41| 343. 整数拆分 |

343. 整数拆分 class Solution { public:int integerBreak(int n) {vector<int> f(n1,0);f[2]1;for(int i3;i<n;i){for(int j1;j<i-1;j){f[i]max(f[i],max(f[i-j]*j,(i-j)*j));}}return f[n];} }; 96. 不同的二叉搜索树 class Solution { public:int numTrees(int…

Flask 使用 JWT(二)

在 Python 使用 JWT 主要的方案是 PyJWT 工具。 安装与基本使用 可以使用 pip 安装 PyJWT: $ pip install pyjwt编码与解码 编码函数 def encode( self, payload: Dict[str, Any], # payload 参数 key: str, …

SkyWalking快速上手(六)——告警

文章目录 前言一、什么是SkyWalking的告警功能二、为什么要使用SkyWalking的告警功能1. 及时发现异常情况2. 提高故障处理效率3. 避免数据丢失和损坏4. 提升系统性能和稳定性 三、如何使用SkyWalking的告警功能1. 告警规则2. 告警通知3. 告警持续时间 四、注意事项1、合理设置告…

23062QTday2

完善登录框 点击登录按钮后&#xff0c;判断账号&#xff08;admin&#xff09;和密码&#xff08;123456&#xff09;是否一致&#xff0c;如果匹配失败&#xff0c;则弹出错误对话框&#xff0c;文本内容“账号密码不匹配&#xff0c;是否重新登录”&#xff0c;给定两个按钮…

vim使用

一、vim配置文件 "查看自己的vimrc所在的目录 "在命令模式下 :echo $MYVIMRC"打开自己的vimrc文件 "在命令模式下 :e $MYVIMRC 二、字体 "查看自己当前的字体及大小 "在命令模式下 :set guifont?"设置默认的字体为仿宋_GB2312&#xff…

ROS 入门

目录 简介 ROS诞生背景 ROS的设计目标 ROS与ROS2 安装ROS 1.配置ubuntu的软件和更新 2.设置安装源 3.设置key 4.安装 5.配置环境变量 安装可能出现的问题 安装构建依赖 卸载 ROS架构 1.设计者 2.维护者 3. 立足系统架构: ROS 可以划分为三层 ROS通信机制 话…

react d3使用循环显示多个地图

首先需要写一个组件&#xff1a; import React, { useState, useEffect, useRef } from react; import ChinaMap from moment; import * as d3 from d3; import chinaGeoJson from china-geojson/src/geojson/china.json;const MyMapToShowResult ({ realData }) > {const…

笔记 | 十六进制不进位加法

这里写自定义目录标题 题目极笨代码错误原因正确代码 题目 Time Limit: 1000 ms Memory Limit: 256 mb 16进制不进位的加法&#xff0c;即和正常加法类似&#xff0c;只是不用去计算进位的数&#xff0c;比正常的加法更简单。 如A 6 0&#xff08;正常加法是10&#xff0c;…

使用JAXB将xml转成Java对象

文章目录 使用JAXB将xml转成Java对象1. xml内容2. Java对象类3. 封装的工具类4. 测试 使用JAXB将xml转成Java对象 工作中遇到个问题&#xff0c;需要将xml转对象&#xff0c;之前复杂的xml都是自己用dom4j来解析组装成Java对象&#xff0c;但是对于简单的&#xff0c;看到了JAX…

SQL中的PowerDesigner逐步深入提问,你能掌握多少?

你提到了有PowerDesigner操作经验&#xff0c;请解释一下PowerDesigner是什么&#xff0c;以及它在数据库设计和开发中的作用是什么&#xff1f; 标准回答&#xff1a; PowerDesigner是一种数据库建模和设计工具&#xff0c;它用于创建数据库模型、设计表结构、定义关系和生成…