深入解析Java 8中HashMap的底层原理

引言

HashMap是Java中常用的集合类,用于存储键值对。其底层实现经过多次优化,包括哈希算法、数组扩容、链表转红黑树等。本文将深入研究HashMap的底层原理,并详细探讨如何解决哈希碰撞的技术。

1. 哈希算法

HashMap的核心是哈希算法,它通过将键的哈希码映射到数组索引,实现快速的数据查找和插入。在JDK 1.8中,哈希算法经过了一些优化,以提高均匀性和减少碰撞的可能性。

2. 数组与链表结构

HashMap的底层数据结构是一个数组,每个数组元素是一个链表(或红黑树)。当多个键映射到相同的索引位置时,它们将被存储在同一个链表中。为了解决哈希碰撞,链表中存储的是一个个键值对。

3. 键值对的存储

HashMap中,键值对以Node对象的形式存储。每个Node包含键、值、哈希码以及指向下一个Node的引用。当产生哈希冲突时,新的Node将被添加到链表的末尾。

4. 解决哈希碰撞的方法

  1. 链地址法:当发生哈希冲突时,将冲突的元素以链表的形式链接在一起,同一个链表上的元素哈希值相同。
    在这里插入图片描述

  2. 红黑树:当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,可以减少查找时间。因为红黑树的时间复杂度为O(logn),而链表为O(n)。

  3. 扩容rehash:当HashMap中的元素数量太多,超过数组大小*加载因子时,会发生扩容。扩容后,需要对原数组中的所有元素重新计算哈希值,然后放到新的扩容后的数组中,这样可以增加链表长度,减少哈希冲突。

  4. 优化哈希算法:JDK 1.8中优化了哈希算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),提高了哈希碰撞分布性。

所以Java 8中HashMap主要通过链地址法+红黑树+扩容rehash+优化哈希算法来解决哈希冲突。这些方法相结合可以有效地解决哈希冲突问题,提高HashMap的性能。

5. 数组扩容机制

HashMap中的元素数量超过容量乘以加载因子时,数组会被扩容。在JDK 1.8中,默认加载因子是0.75。扩容涉及到重新计算哈希码、重新分配数组,并将现有元素重新放置到新的数组中。这确保了HashMap的性能和空间的平衡。

6. 红黑树的引入

为了应对链表过长的情况,JDK 1.8引入了红黑树。当链表长度达到8时,链表将被转换为红黑树,以提高查找效率。红黑树的引入使得在最坏情况下,查找时间复杂度从O(n)降低到O(log n)。

为什么当链表长度达到8时,链表将被转换为红黑树,又为什么红黑树转链表的阈值为6?
首先和hashcode碰撞次数的泊松分布有关,主要是为了实现时间和空间的平衡,在负载因子为0.75默认情况下,单个hash槽内元素个数为8的概率小于百万分之一,将7作为一个分水岭,等于7时不做转换,大于等于8才转红黑树,小于等于6才转链表,链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的,红黑树中的TreeNode,是链表中的Node所占空间的2倍,虽然红黑树的查找效率为o(logN),要优于链表的o(N),但是当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高,所以,要寻找一种时间和空间的平衡,即在链表长度达到一个阈值,之后再转换为红黑树,之所以是8,是因为Java的源码贡献者,在进行大量实验发现,hash碰撞发生8次的概率,已经降低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素,本身和hash函数的原因,此次操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞,所以,这个时候,就应该将链表转换为红黑树了,也就是为什么链表转红黑树的阈值是8;
最后,红黑树转链表的阈值为6,主要是因为:如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源。

7. 在Java 8中的实现细节

在JDK 1.8中,HashMap的实现经过了优化,包括更好的哈希算法、红黑树的引入、链表长度的控制等。这些变化使得HashMap在面对各种情况时都能提供高效的性能。

8. 性能优化与注意事项

在使用HashMap时,需要注意一些性能优化的问题,例如合理选择初始容量和加载因子、避免频繁扩容等。对于特定的应用场景,可以通过调整这些参数来达到更好的性能。

HashMap,ArrayMap,SparseArray的区别?

在 Android 开发中,HashMapArrayMapSparseArray 都是用来存储键值对的数据结构,但它们在内部实现和使用场景上有所不同。下面是每种数据结构的特点和使用场景:

1. HashMap

  • 实现方式HashMap 是一个标准的 Java 集合类,使用散列函数将键映射到其值。它允许使用任何类型的对象作为键和值。
  • 性能特点
    • 在处理大量数据时性能较好。
    • 由于使用散列表,其 getput 操作通常提供恒定的时间性能。
  • 内存使用:相比于 ArrayMapSparseArrayHashMap 在内存上的开销更大,主要是因为它使用了更多的对象引用和额外的数据结构。

2. ArrayMap

  • 实现方式ArrayMap 是 Android 提供的一个集合类,专为 Android 优化。它通过两个数组实现:一个用于键,另一个用于值。
  • 性能特点
    • 在数据量较小(通常少于几千项)时性能较好。
    • getput 操作的时间复杂度大约是 O(log n),因为它使用二分查找来维护键的排序顺序。
  • 内存使用ArrayMap 旨在减少内存占用,尤其是当存储的元素数量较少时。

3. SparseArray

  • 实现方式SparseArray 是 Android 特有的数据结构,用于在你的键是 int 类型时替代 HashMap<Integer, Object> 的使用。它使用两个数组,一个用于存储键,另一个用于存储值。
  • 性能特点
    • 类似于 ArrayMap,适用于数据量较小的情况。
    • getput 操作的时间复杂度同样是 O(log n)。
  • 内存使用SparseArray 在处理整数键的映射时更为高效,因为它避免了对整数键的自动装箱,减少了内存占用。

使用场景

  • HashMap:当你需要存储大量数据,或者键和值不是基本类型或其包装类时,HashMap 是一个好选择。
  • ArrayMap:在数据量不是特别大,且关注内存优化的 Android 开发中使用。适合那些对性能要求不是非常高的场景。
  • SparseArray:当键为 int 类型,且你希望减少内存开销时,使用 SparseArray 更合适。

总的来说,选择哪种数据结构取决于你的具体需求,包括数据的大小、性能要求以及内存优化的考虑。

Jdk的1.8中,HashMap,里面的查找算法的时间复杂度是什么?

在 JDK 1.8 中,HashMap 的查找算法时间复杂度在不同情况下有所不同:

  1. 最佳情况:当哈希函数分布均匀时,理想情况下每个桶(bucket)只有一个元素,或者元素均匀分布在不同的桶中。在这种情况下,查找操作的时间复杂度接近 O(1),即常数时间。

  2. 最坏情况:如果多个元素都映射到同一个桶中,并且哈希碰撞处理不当,那么在最坏的情况下,查找操作的时间复杂度可以退化到 O(n),其中 n 是 HashMap 中元素的数量。这种情况通常发生在哈希函数的质量不高或者键分布不均匀时。

  3. JDK 1.8 的改进:在 JDK 1.8 中,为了优化这种最坏情况的性能,当某个桶中元素数量过多(超过一定阈值)时,这些元素会从链表结构转换为红黑树结构。这种改进意味着即使在最坏的情况下,当元素过多集中在一个桶时,查找操作的时间复杂度也会是 O(log n)。但请注意,这里的 n 指的是单个桶中元素的数量,而不是整个 HashMap 的大小。

总体来说,HashMap 的查找效率通常很高,但它依赖于良好的哈希函数和均匀的键分布。JDK 1.8 中的改进进一步提高了在不理想情况下的性能。

结论

HashMap作为Java中常用的数据结构之一,在JDK 1.8中经过了一系列的优化和改进。深入理解其底层原理,包括哈希算法、数组与链表结构、红黑树的引入等,以及如何解决哈希碰撞的技术,有助于更好地使用和理解HashMap的性能特性。在实际应用中,根据具体场景选择适当的参数,可以更好地发挥HashMap的优势,提高程序的性能和效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值