自学内容网 自学内容网

Java集合篇——HashMap

HashMap 是 Java 中最常用的数据结构之一,它实现了 Map 接口,用于存储键值对。HashMap 允许 null 值和 null 键,并且通过哈希表的方式实现,能够提供常数时间复杂度的 O(1) 查找和插入操作(在理想情况下)。它在处理大量数据时性能非常高。

1. HashMap 的基本结构

(a) 底层数据结构:数组 + 链表(链地址法)

HashMap 底层通过一个 数组 来存储键值对,每个数组元素称为 (bucket)。

每个桶中存储的是 链表红黑树(Java 8 之后引入红黑树来优化性能),当发生哈希冲突(多个键的哈希值映射到同一个位置)时,这些键值对会以链表或红黑树的形式存储在同一个桶中。

(b) 哈希表原理

每个键通过其哈希值(hashCode())计算出在数组中的索引。HashMap 通过公式来计算数组下标:

index = (n - 1) & hash 

其中,n 是数组的长度,hash 是键的哈希值,& 是按位与操作符。这种计算方式可以确保键的哈希值分布在数组的不同位置上。

(c) 负载因子与容量

HashMap 有两个重要的参数:初始容量负载因子

数组初始容量的默认大小是 16。负载因子的默认值是 0.75。当 HashMap 中元素数量超过数组长度 ×负载因子时,HashMap 会进行扩容,将数组长度加倍并重新分配所有键值对到新的位置。

(d) 链表转红黑树

在 Java 8 之前,当多个键的哈希值相同并存储在同一个桶中时,HashMap 使用链表来存储这些键值对。

Java 8 引入了红黑树来优化查找性能。当链表长度超过一定阈值(默认是 8),链表会转换为红黑树,以提高查找速度从 O(n) 下降到 O(log n)

2. 插入、查找和删除过程

(a) 插入过程

首先调用键的 hashCode() 方法计算哈希值。根据哈希值计算出键值对在数组中的索引位置。

在是检查是否有冲突,查看对应位置是否已经有数据(哈希冲突)。如果没有冲突,则将键值对直接插入该位置。如果有冲突(多个键的哈希值映射到同一个桶),则根据情况:如果链表长度超过阈值,则链表转换为红黑树,键值对插入红黑树中。如果是链表,则将键值对添加到链表末尾。

最后检查负载因子,插入完成后,检查 HashMap 的大小是否超过了负载因子阈值,如果超过,则进行 扩容。

(b) 查找过程

计算哈希值:根据键的 hashCode() 计算出哈希值。使用哈希值计算键在数组中的索引位置。遍历链表或红黑树,找到该位置后:如果是链表,则遍历链表,找到对应的键。如果是红黑树,则通过树的结构查找对应的键。

(c)删除过程

首先,HashMap使用哈希函数对目标键(Key)进行哈希值计算。哈希值决定了键可能所在的桶(数组索引)。计算公式如下:

int hash = hash(key);

int index = (n - 1) & hash;

其中n是桶数组的长度,hash(key)是对键应用哈希函数的结果。通过该计算,定位目标键的桶。

在遍历桶中的链表或树,找到对应的桶后,HashMap需要遍历这个桶内的所有元素(在存在哈希冲突的情况下)。桶中的元素可以是:

链表节点:如果桶中是链表,遍历链表查找目标键。

红黑树节点:如果桶中是红黑树,则使用红黑树的搜索算法查找目标键。

最后是删除操作,根据找到的节点类型,remove有不同的处理方式:

链表删除:如果目标键位于链表中,HashMap会修改链表指针,将目标节点从链表中移除。通常的删除方式是将当前节点的前一个节点指向它的下一个节点,以此跳过要删除的节点。如果是链表的头节点,直接将头节点替换为下一个节点。

红黑树删除:如果目标键位于红黑树中,删除操作会涉及红黑树的删除平衡调整。红黑树删除较为复杂,需要根据树的特性进行重新平衡。

调整大小和清理

桶数组更新:一旦删除成功,HashMap会更新桶数组中的引用,确保指针的正确性。

树化和去树化:如果在一个桶中删除某个节点后,红黑树的节点数量减少到阈值以下,HashMap会将红黑树重新转化为链表。

返回删除的值

remove方法会返回与被删除键对应的值(如果存在)。如果目标键不在HashMap中,则返回null

在上面的插入过程中,如果不同键 Key1Key9 的哈希值计算后得到的数组下标相同,它们会形成一个链表。随着数据量增大,链表长度过长时,链表可能转为红黑树。

3. 扩容机制

HashMap 中存储的元素数量超过 数组容量 × 负载因子 时,HashMap 会自动进行 扩容。扩容的过程如下:

        创建一个新的数组,容量是原来的两倍。

        将旧数组中的所有键值对重新计算哈希并放入新数组中。这是因为扩容后数组大小发生变化,原有的哈希值对应的数组下标可能会改变。

        重新分配后,旧数组中的链表或红黑树会被拆分并重新放入新数组中。

扩容虽然能避免数组过满导致哈希冲突增多,但会带来较大的性能消耗,尤其是当数据量很大时,频繁的扩容会导致性能下降。因此,合理设置初始容量是优化 HashMap 性能的一个重要手段。

4. HashMap 的线程安全问题和应用场景

HashMap 本身并不是线程安全的。在多线程环境下,多个线程同时访问和修改 HashMap 可能会导致数据不一致或死循环。因此,在多线程场景下建议使用 ConcurrentHashMap,它是线程安全的 HashMap 实现。

HashMap 常用于需要通过键值对快速查找数据的场景。典型应用场景包括:

缓存:将常用数据缓存到内存中以加快查找速度。

配置存储:存储配置文件中的键值对,例如应用配置、用户设置等。

计数器:例如,统计单词出现的次数,每个单词作为键,出现次数作为值。

优缺点:

优点:插入、查找、删除操作非常快,适合大规模数据的处理。灵活支持 null 值和 null 键。

缺点:不是线程安全的,需在多线程场景中小心使用。扩容时可能会带来性能损耗。


原文地址:https://blog.csdn.net/sdg_advance/article/details/142891641

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!