Hashmap底层首先就是基于数组实现,后面就可以围绕数组进行解释。
Hash算法的优化:
// jdk1.8之后的原代码
Return (key==null) ?0 :(h=key.hashcode())^(h >>>16))
1111 1111 1111 1111 1111 1010 0111 1100 key.hashcode
0000 0000 0000 0000 1111 1111 1111 1111 h >>>16
^
1111 1111 1111 1111 0000 0101 1000 0011 转为int值返回
为什么这样处理呢?异或是结果值得低16位同时保证了高16位与低16位的特征,可以再后续的&运算都可以参与到运算之中,减少hash冲突。
寻址算法:
(n-1)&hash 得到数据的一个位置 n约定为16 取模运算的他是性能差了一些,为了优化寻址算法,hash &(n-1)效果和hash对n的取模效果一样,但是于运算性能比hash对n取模高,数学问题,数组的长度为2的次方的时候,只要保持数组长度为2的n次方
0000 0000 0000 0000 0000 0000 0000 1111 (n-1)的二进制
1111 1111 1111 1111 0000 0101 1000 0011 (优化后的二进制)
1111 1111 1111 1111 1111 1010 0111 1100 (没有优化的二进制)
&
则就是高16位实际上没有参与到运算里面,全部都是低16位的运算参与了。但是优化后的hash值保证了高低16位都参与了运算,从而减少了hash碰撞。
寻址算法的优化:用与运算替代取模,提升性能。
Map.put和map.get 算出得hash值,到寻址算法找到一个数组得位置,把key-value放进数组,或者从数组中取出来。但是如果两个key算出的位置是一样的,那么就涉及到了hash碰撞,那么这个时候就会在这个位置挂一个链表,把这个链表里面放入元素,如果get的时候,只需要定位到这个位置的链表,遍历链表,从里面找出自己需要的值。链表很长,那么性能越差(时间复杂度位O(n))
如果这个链表的长度达到了一定的长度之后,把链表转为红黑树,遍历一颗红黑树找到一个元素,时间复杂度位O(logn)。条件:当链表长度超过8的时候,转为红黑树,当红黑树的长度小于6的时候,转为链表。
hash桶中存放的链表长度概率 随着长度的增加而减小
hashmap中的源码注释
(二) 为什么到8转为红黑树 到6转为链表
TreeNodes(红黑树)占用空间是普通Nodes(链表)的两倍,为了时间和空间的权衡。
节点的分布频率会遵循泊松分布,链表长度达到8个元素的概率为0.00000006,几乎是不可能事件.
为什么转化为红黑树的阈值8和转化为链表的阈值6不一样,是为了避免频繁来回转化。
数据扩容:
重新扩容之后,会再次进行&运算,判断二进制结果中是否多一个bit的1,如果没有,那么就是原位置,否则就是index+oldgap,通过这个方式就避免了rehash的时候,每个hash对length取模。