如果说String是我们用得最多的数据类型,那么HashMap绝对算得上是用得最多的数据结构了。

HashMap map = new HashMap(4),我们往map里不断put你有没有想过这个map里装不下数据了怎么办?我们执行get方法好像性能还挺快,这是为什么?

HashMap的底层核心数据结构

java中map取最大值 java map最大长度_寻址

HashMap底层核心数据结构是数组,数组里的数据类型是HashMap.Node,既然是数组那么就有个数组长度的概念。阿里Java开发规范里要求在构造HashMap时要指定初始化数组的长度,new HashMap(4)这个4就是数组的初始数组长度。如果你不指定的话,默认是长度是16。HashMap.Node是个单向链表,画一张图来看下HashMap整体的数据结构是个什么样的。

java中map取最大值 java map最大长度_java中hashmap深入解析_02

JDK8之后做了优化,当链表长度大于8以后将变成红黑树,后文hash碰撞带来性能问题里会讲到。

put方法执行逻辑

我们执行put方法,比如put("张三","很高")

会先计算张三这个key的hash值;然后将hash值与数组长度进行取模,取模后的值就是数组里的元素对应的索引index;如果数组对应index位置还没有元素,那么直接构建一个HashMap.Node对象封装“张三”和“很高”放入该数组的index位置;如果index位置已经有元素存在,那么比较将要put进入的元素的key值计算出来的hash值与已经存在的元素的key对应的hash值是否相同;如果相同则更新该HashMap.Node对象的value值,如果不相同,则构建一个HashMap.Node对象,并将其放入单向链表的尾部。get方法执行逻辑

我们执行get方法,比如get("张三"),

也会先对张三这个key计算hash值;然后将hash值对数组长度进行取模,定位到数组中的一个元素;如果该数组元素只有一个HashMap.Node节点,则直接将其value返回;如果该数组元素存在多个HashMap.Node节点,则遍历由HashMap.Node组成的单向链表,找出key与传入的key相等的(“张三”)HashMap.Node并将其value返回。什么是hash碰撞问题,如何解决

需要明确的一点是,hash碰撞是无法避免的,使用hash算法只可能尽量地产生hash碰撞的几率。

在HashMap中,当多个key计算出的hash相等时,实际上会在计算出的数组元素的位置维护一个单向链表,当链表长度越长,执行get操作的性能就越差,最坏的情况是需要将整个链表都遍历一遍才能获取到目标值,其时间复杂度位O(n)。

对此,HashMap用红黑树进行了优化。当执行put操作时,当链表中的元素大于8个时,会将当前需要执行put操作的元素放入链表后,将该链表转化为红黑树。转化过后其指定get操作时的时间复杂度为O(log n),相较于链表的O(n)有所提升。

JDK8对HashMap的hash算法和寻址算法优化

HashMap的hash算法和寻址算法之间存在一定的联系,寻址算法是通过hash值在底层数组中快速定位到一个具体的数组元素。看下JDK8后的算法是怎样的

1、hash算法:(h = key.hashCode()) ^ h >>> 16

java中map取最大值 java map最大长度_java中hashmap深入解析_03

先将初始hash右移16位,将初始hash值的高16位与低16位对齐;接下来是对初始hash值和右移16位后的hash值进行异或运算。目的是让计算出来的hash值的低16位同时包含初始hash值高低16位的特征,为寻址算法提供一个尽可能完美的基础值。这是HashMap开发者对大部分场景下数据存取量不会超过65536个而作出的考量。

2、寻址算法:(n - 1) & hash

我们采用对照实验的方式,分别提供两个初始hash值和两个hash算法优化后的hash值,对其执行寻址算法的运算。默认情况下HashMap中数组的大小为16,因此这里设n=16。

java中map取最大值 java map最大长度_寻址_04

实验结果分析:从实验数据来看,两个初始hash值仅在第17位存在差异;从实验结果来看,采用寻址算法对两个初始hash值进行运算后得出了相同的结果,原因是,这两个初始hash值的低16位完全相同,导致在与(n-1)的二进制码进行与运算时,第17位的差异被忽略了。需要注意的是,在大多数应用场景下,(n-1)的值不会大于65536,即低16位能表示的元素总量。而使用hash算法优化后的hash值进行运算,由于低16位中同时包含了初始hash值高低16位的特征,因此与(n-1)的二进制码进行与运算后,二者的计算结果不相同。

实验结论:优化hash算法的出发点,是考虑到大多数场景下寻址算法中的(n-1)都不会大于65536,换而言之,大多数场景下hash值与(n-1)的二进制码进行与运算,都是在低16位中(意味着大多数情况下,高16位全是0,这时候不论hash值的高16位是什么,与运算的最终结果也只会是0)。因此,需要想办法,避免hash值高16位中的差异被忽略的情况,而HashMap中的解决方案,便是将初始hash值右移16位,并与初始hash值执行异或运算,让hash值的低16位中同时包含高低16位的特征。

3、hash算法和寻址算法的优化总结

hash算法通过将初始hash值右移16位,并与初始hash值执行异或运算,使优化后的hash值的低16位中同时包含初始hash值高低16位的特征,避免了初始hash值的高16位在于(n-1)的二进制码执行与运算时被忽略的情况,根本目的在于降低hash碰撞的几率;用"(n-1)&hash"替代取模运算,因为与运算的效率远高于取模运算,目的是提升寻址性能;如何进行扩容

HashMap的构造函数里有个叫装在因子的参数loadFactor,它默认值是0.75。

java中map取最大值 java map最大长度_数组_05

HashMap的底层的数组默认长度是16,当数组中的元素值达到一定的阈值(16*0.75=12)的时候便会触发2倍扩容,具体操作是创建一个新数组,其容量是原数组的2倍,并将原数组中的元素拷贝到新数组中。

在执行拷贝之前,会执行rehash操作,重新计算数组元素在新数组中的位置。具体操作为:

如果数组元素中只有一个Node节点,则直接对该Node节点执行"e.hash&newCap-1"重新计算其在新数组中的位置;如果数组元素中包含多个Node节点,且该数组元素为红黑树,则将树上的节点执行rehash之后根据hash值放到新数组中。如果重新分配位置后,新的红黑树的节点数量小于等于6,则将红黑树转化为链表;如果数组元素中包含多个Node节点,且该数组元素不是红黑树,则对这多个节点进行遍历,通过"e.hash&oldCap"进行运算,判断节点在新数组中的位置是否与现位置相同,该操作将该数组元素中的多个节点分为了两部分,一部分在新数组中的位置与之前相同,一部分在新数组中的位置为"原位置+oldCap"。e.hash&oldCap操作的意义

这步操作的意义在于,判断节点hash值的高一位的二进制码是否为1,只有该二进制码为1的时候,进行寻址运算时位置才可能发生变化。因此只要出现"n&hash != 0",就意味着原有hash进行寻址算法运算时的结果值发生了变化,即该节点在新数组中的位置会发生变化。

java中map取最大值 java map最大长度_java中hashmap深入解析_06

为什么部分节点在新数组中的新位置是"原位置+oldCap"

java中map取最大值 java map最大长度_java中map取最大值_07

这意味着,在节点hash值不变的情况下,执行"(n-1)&hash"运算,其结果只是在原有计算结果的基础上加高一位二进制码代表的值,如第5位二进制码代表16(即原容量的值)。即"原位置+oldCap"。