引言
HashMap
是Java中常用的集合类,用于存储键值对。其底层实现经过多次优化,包括哈希算法、数组扩容、链表转红黑树等。本文将深入研究HashMap
的底层原理,并详细探讨如何解决哈希碰撞的技术。
1. 哈希算法
HashMap
的核心是哈希算法,它通过将键的哈希码映射到数组索引,实现快速的数据查找和插入。在JDK 1.8中,哈希算法经过了一些优化,以提高均匀性和减少碰撞的可能性。
2. 数组与链表结构
HashMap
的底层数据结构是一个数组,每个数组元素是一个链表(或红黑树)。当多个键映射到相同的索引位置时,它们将被存储在同一个链表中。为了解决哈希碰撞,链表中存储的是一个个键值对。
3. 键值对的存储
在HashMap
中,键值对以Node
对象的形式存储。每个Node
包含键、值、哈希码以及指向下一个Node
的引用。当产生哈希冲突时,新的Node
将被添加到链表的末尾。
4. 解决哈希碰撞的方法
-
链地址法:当发生哈希冲突时,将冲突的元素以链表的形式链接在一起,同一个链表上的元素哈希值相同。
-
红黑树:当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,可以减少查找时间。因为红黑树的时间复杂度为O(logn),而链表为O(n)。
-
扩容rehash:当HashMap中的元素数量太多,超过数组大小*加载因子时,会发生扩容。扩容后,需要对原数组中的所有元素重新计算哈希值,然后放到新的扩容后的数组中,这样可以增加链表长度,减少哈希冲突。
-
优化哈希算法: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 开发中,HashMap
、ArrayMap
和 SparseArray
都是用来存储键值对的数据结构,但它们在内部实现和使用场景上有所不同。下面是每种数据结构的特点和使用场景:
1. HashMap
- 实现方式:
HashMap
是一个标准的 Java 集合类,使用散列函数将键映射到其值。它允许使用任何类型的对象作为键和值。 - 性能特点:
- 在处理大量数据时性能较好。
- 由于使用散列表,其
get
和put
操作通常提供恒定的时间性能。
- 内存使用:相比于
ArrayMap
和SparseArray
,HashMap
在内存上的开销更大,主要是因为它使用了更多的对象引用和额外的数据结构。
2. ArrayMap
- 实现方式:
ArrayMap
是 Android 提供的一个集合类,专为 Android 优化。它通过两个数组实现:一个用于键,另一个用于值。 - 性能特点:
- 在数据量较小(通常少于几千项)时性能较好。
get
和put
操作的时间复杂度大约是 O(log n),因为它使用二分查找来维护键的排序顺序。
- 内存使用:
ArrayMap
旨在减少内存占用,尤其是当存储的元素数量较少时。
3. SparseArray
- 实现方式:
SparseArray
是 Android 特有的数据结构,用于在你的键是int
类型时替代HashMap<Integer, Object>
的使用。它使用两个数组,一个用于存储键,另一个用于存储值。 - 性能特点:
- 类似于
ArrayMap
,适用于数据量较小的情况。 get
和put
操作的时间复杂度同样是 O(log n)。
- 类似于
- 内存使用:
SparseArray
在处理整数键的映射时更为高效,因为它避免了对整数键的自动装箱,减少了内存占用。
使用场景
- HashMap:当你需要存储大量数据,或者键和值不是基本类型或其包装类时,
HashMap
是一个好选择。 - ArrayMap:在数据量不是特别大,且关注内存优化的 Android 开发中使用。适合那些对性能要求不是非常高的场景。
- SparseArray:当键为
int
类型,且你希望减少内存开销时,使用SparseArray
更合适。
总的来说,选择哪种数据结构取决于你的具体需求,包括数据的大小、性能要求以及内存优化的考虑。
Jdk的1.8中,HashMap,里面的查找算法的时间复杂度是什么?
在 JDK 1.8 中,HashMap
的查找算法时间复杂度在不同情况下有所不同:
-
最佳情况:当哈希函数分布均匀时,理想情况下每个桶(bucket)只有一个元素,或者元素均匀分布在不同的桶中。在这种情况下,查找操作的时间复杂度接近 O(1),即常数时间。
-
最坏情况:如果多个元素都映射到同一个桶中,并且哈希碰撞处理不当,那么在最坏的情况下,查找操作的时间复杂度可以退化到 O(n),其中 n 是
HashMap
中元素的数量。这种情况通常发生在哈希函数的质量不高或者键分布不均匀时。 -
JDK 1.8 的改进:在 JDK 1.8 中,为了优化这种最坏情况的性能,当某个桶中元素数量过多(超过一定阈值)时,这些元素会从链表结构转换为红黑树结构。这种改进意味着即使在最坏的情况下,当元素过多集中在一个桶时,查找操作的时间复杂度也会是 O(log n)。但请注意,这里的 n 指的是单个桶中元素的数量,而不是整个
HashMap
的大小。
总体来说,HashMap
的查找效率通常很高,但它依赖于良好的哈希函数和均匀的键分布。JDK 1.8 中的改进进一步提高了在不理想情况下的性能。
结论
HashMap
作为Java中常用的数据结构之一,在JDK 1.8中经过了一系列的优化和改进。深入理解其底层原理,包括哈希算法、数组与链表结构、红黑树的引入等,以及如何解决哈希碰撞的技术,有助于更好地使用和理解HashMap
的性能特性。在实际应用中,根据具体场景选择适当的参数,可以更好地发挥HashMap
的优势,提高程序的性能和效率。