HashMap是一个数组链表和红黑树的结合体 HashMap的第一层表现是数组,HashMap默认创建一个长度为十六的数组来储存数据,但不同的是,它并非是先放在第0个索引,然后第一个索引那么放置,而是通过key获取对应的32位hash值,然后高十六位和低十六位进行异或操作,然后用结果的后四位和15的二进制(也就是1111)进行与操作,得到一个0-15之间的值来匹配索引位置。
当匹配到的索引位置已经有值时,他会和当前索引中保存的所有key通过equal方法进行判断是否相等,如果相等则进行覆盖,不相等时,则保存在最后一个值的next属性中,作为链表保存。
当一个链表过长时,注定会影响查询性能,此时则需要一个能够平衡性能的数据结构进行保存,当链表长度大于8时,链表变为被转换为红黑树保存,同理,当数据被删除的过少时,也会被转换为链表,不过不是8,而是6,当数据量为六个时,红黑树又会被转化为链表。这相当于在两个变换过程中加了一个缓冲区,防止因为删除和新增操作造成的频繁变换,造成性能损耗。
HashMap默认创建十六长度的数组,我们也可以自定义长度,不过整个长度必须是2的n次幂,因为存放索引是通过二进制的异或操作结果确定的,如果数组长度不是二进制,那么会导致部分长度无法访问,或者是部分长度根本不存在的问题(当我们调用HashMap的构造方法时,如果不传入2的n次幂的整数,他则会寻找最近的值作为HashMap的初始长度)
HashMap也会进行扩容,当数组容量超过负载因子的百分比时(默认是百分之七十五,可以创建HashMap时自己设置),HashMap会进行容量乘二的扩容,但因为容量的变化,所以各个hash值的索引也会改变,所有元素都需要重新存储(即使没有这个原因,也要重新存储,因为数组是一个连续的内存,想要扩容数组只有一边办法,那就是找到更长的连续内存,创建一个新数组)。
了解完HashMap后实际上会有一些疑问,比如说,他在处理索引时会进行复杂的操作,他会通过扰动函数(也就是说之前说的用hashcode的高十六位和第十六位进行异或操作)得到一个新的值,在将新的值的后四位和15的二级制进行与操作,这一步的意义是什么呢?我为什么不直接取hash值的后四位呢?官方解释是让其分布更平均,那为什么他生成的hash值的后四位就不平均呢?
猜测可能有两种可能,一种是hash值的生成本身就有缺陷,后四位可能更倾向于某个位置,第二种可能是hash值的生成和key有着比较规律性的关联,而在一个项目中的key通常又很相似,所以会造成最终的hash值后四位比较相似。
HashMap源码
首先HashMap的put方法会调用hashcode函数来获取hash值
我们进入hash方法,如下
这一步是在将获取的hashcode的高位和低位进行异或运算,这也是我们上面说的扰动函数,其中hashCode方法就是在获取hash值,我们进入hashCode
可以看到这个方法没有做实现,首先这绝对不是一个接口,没有实现是因为hashCode方法的是实现是内置在JVM中的c++语言实现的,所以在java代码中看不到。当获取完hash值后,我们便可以进入第一步的putVal方法中进行添加key和value了。
其中Node类就是我们保存的一个值,而tab是Node的数组,也可以知道他就是最终保存数据的结构,其中Node类内容如下
可以看到Node的属性包括hash值,key,value,以及next用来保存下一位节点的引用
了解了Node的结构后,在看回putVal的代码
当数据量超过负载因子规定时则调用resize方法
HashMap源码中的红黑树转换以及新增删除逻辑极为复杂,这里就不说了。