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.

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

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

【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;让浏览器自动加载页…

23062QTday2

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

ROS 入门

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

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

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

【漏洞复现】Smanga未授权远程代码执行漏洞(CVE-2023-36076) 附加SQL注入+任意文件读取

文章目录 前言声明一、产品简介一、漏洞描述二、漏洞等级三、影响范围四、漏洞复现五、修复建议六、附加漏洞漏洞一、SQL注入漏洞二、任意文件读取 前言 Smanga存在未授权远程代码执行漏洞,攻击者可在目标主机执行任意命令,获取服务器权限。 声明 请勿利用文章内的相关技术从…

windows彻底卸载unity

1.控制面板卸载 双击打开桌面的控制面板&#xff0c;选择卸载程序&#xff0c;选中Unity和UnityHub右击卸载。 2.清除unity的注册表 在运行中输入“regedit”双击打开注册表界面 删除 HKEY_CURRENT_USER\Software\Unity 下所有项 删除 HKEY_CURRENT_USER\Software\Unity Tec…

电脑怎么取消磁盘分区?

有时候&#xff0c;我们的电脑会出现一个磁盘爆满&#xff0c;但另一个却空着&#xff0c;这时我们可以通过取消磁盘分区来进行调整&#xff0c;那么&#xff0c;这该怎么操作呢&#xff1f;下面我们就来了解一下。 磁盘管理取消磁盘分区 磁盘管理是Windows自带的磁盘管理工具…

BMS电池管理系统的蓝牙芯片 国产高性能 低功耗蓝牙Soc芯片PHY6222

电池管理系统是对电池进行监控与控制的系统&#xff0c;将采集的电池信息实时反馈给用户&#xff0c;同时根据采集的信息调节参数&#xff0c;充分发挥电池的性能。但是&#xff0c;前技术中&#xff0c;在管理多个电池时&#xff0c;需要人员现场调试与设置&#xff0c;导致其…

uniapp h5网页打开白屏

修改了默认基本运行路径&#xff0c;然后直接打开index.html的情况下是会这样&#xff0c;放在nginx服务器上运行就ok了。 把默认的./ 路径修改了&#xff1a;/cloudh5 nginx html目录下放子网站 &#xff1a;/cloudh5&#xff1a;

6-2 pytorch中训练模型的3种方法

Pytorch通常需要用户编写自定义训练循环&#xff0c;训练循环的代码风格因人而异。&#xff08;养成自己的习惯&#xff09; 有3类典型的训练循环代码风格&#xff1a;脚本形式训练循环&#xff0c;函数形式训练循环&#xff0c;类形式训练循环。 下面以minist数据集的多分类模…

Spring Boot集成Redis实现数据缓存

&#x1f33f;欢迎来到衍生星球的CSDN博文&#x1f33f; &#x1f341;本文主要学习Spring Boot集成Redis实现数据缓存 &#x1f341; &#x1f331;我是衍生星球&#xff0c;一个从事集成开发的打工人&#x1f331; ⭐️喜欢的朋友可以关注一下&#x1faf0;&#x1faf0;&…

【最新面试问题记录持续更新,java,kotlin,android,flutter】

最近找工作&#xff0c;复习了下java相关的知识。发现已经对很多概念模糊了。记录一下。部分是往年面试题重新整理&#xff0c;部分是自己面试遇到的问题。持续更新中~ 目录 java相关1. 面向对象设计原则2. 面向对象的特征是什么3. 重载和重写4. 基本数据类型5. 装箱和拆箱6. …

华为aarch64架构的泰山服务器EulerOS 2.0 (SP8)系统离线安装saltstack3003.1实践

华为泰山服务器的CPU芯片架构为aarch64&#xff0c;所装系统为EulerOS 2.0 (SP8)aarch64系统&#xff0c;安装saltstack比较困难。本文讲解通过pip安装方式离线安装saltstack3003.1以进行集中化管理和维护。 一、系统环境 1、操作系统版本 [rootlocalhost ~]# cat /etc/os-r…

uniapp 可输入可选择的........框

安装 uniapp: uni-combox地址 vue页面 <uni-combox :border"false" input"selectname" focus"handleFocus" blur"handleBlur" :candidates"candidates" placeholder"请选择姓名" v-model"name"&g…

在微信公众号怎么实现投票活动

微信公众号实现投票活动的方法和步骤 一、投票活动的优势 通过投票活动&#xff0c;微信公众号可以实现用户参与、增加互动、了解用户需求等功能&#xff0c;同时也可以提升品牌知名度和用户粘性。以下是一些投票活动的优势&#xff1a; 增加用户参与度&#xff1a;通过投票活…