HashMap 集合简介
HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。
jdk1.8 之前 HashMap 由 数组 + 链表 组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。
什么是哈希冲突? 你可以把hash想象成一个数组,现在你想把一个数据存到hash表中。那么问题来了:这个数据应该存到哪里?
于是,你需要一个hash函数,这个函数的作用就是把你要存的数据映射成hash表中的一个位置,这个位置就是你要存放该数据的地方。一般把hash表的每个位置都叫做“槽(slot)”,很形象,你要往槽里放数据。假如你要存的数据为k,存放在哪个槽里呢?很简单,存在hash(k)这个槽里。
这个hash函数是你自定义的。这里我以《算法导论》里面的一个题目举例:现在你选的hash函数是这样的(取模法): hash(k) = k mod 9 假设hash表的大小为9(即有9个槽),现在要把一串数据存到表里:5,28,19,15,20,33,12,17,10
简单计算一下:hash(5)=5, 所以数据5应该放在hash表的第5个槽里;hash(28)=1,所以数据28应该放在hash表的第1个槽里;hash(19)=1,也就是说,数据19也应该放在hash表的第1个槽里——于是就造成了碰撞(也称为冲突,collision)。 影响产生冲突多少有以下三个因素: 1.散列函数是否均匀;2.处理冲突的方法;3.散列表的装填因子。 如果数据的分布比较广泛,而且储存数据的数组长度比较大,那么哈希冲突就比较少,否则冲突是很高的。 哈希冲突的常用解决办法:链式地址法
补充:将链表转换成红黑树前会判断,即便阈值大于 8,但是数组长度小于 64,此时并不会将链表变为红黑树,而是选择逬行数组扩容。
这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要逬行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能和减少搜索时间,底层阈值大于8并且数组长度大于64时,链表才转换为红黑树,具体可以参考 treeifyBin() 方法。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于 8 并且数组长度大于 64 时,链表转换为红黑树时,效率也变的更高效。
小结:HashMap 特点:
- 存储无序的。
- 键和值位置都可以是 null,但是键位置只能存在一个 null。
- 键位置是唯一的,是底层的数据结构控制的。
- jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树。
- 阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。
|