HashMap
概述
- 结构:数组+链表or红黑树
- 链表->红黑树:8,红黑树->链表:6
- 数组大小:2的倍数,方便通过&代替%操作:
寻址:i = (n - 1) & hash 等价于 hash%(n-1) ,n是长度
-
扩容:大小大于threshold
-
扰动函数
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。
-
扩容:
- 一定是2的倍数,下一次扩容是本次容积的2倍
-
为什么不直接用红黑树,而是链表+红黑树?
- 根据泊松分布,如果hashCode方法合适,那么其实一个bin出现8个以上hash碰撞的概率是很小的
- 而且红黑树节点消耗资源约为普通节点的2倍
- 红黑树维护成本大于链表,使用put方法的时候需要注意
-
多线程安全性
总体来说多线程不安全。
-
1.8问题相对小一些,可能出现数据读写不一致的问题;
-
1.7的问题,是因为1.7中数据是直接接到原来的头节点前面的,而1.8是加在后面。
- 那么此时如果是多线程修改该bin,可能会导致循环链表的产生。具体分析可见:juejin.cn/post/684490…
- 至于为什么是头插法,原因是基于跟LRU一样的假设:最近新增的元素,被访问的概率更大。
核心方法
-
put
-
先判断数组是否为空
- 为空则新建
-
随后根据寻址(hash&(length-1))查找位置
-
如果为空则直接塞值
-
不为空:
-
如果和第一个节点相同:替换
-
如果是树节点:树节点查找节点
-
如果是链表:查找,如果key相同则返回相同的节点,如果没有相同则新建节点连接在后面
- 此时如果大于树化阈值,则进入节点树化方法
-
-
-
根据上一步得到的老节点(key相同的),赋值;
-
如果上一步得到的老节点为null,那么判断大小是否大于Threshold需要扩容。
- 这里需要注意:threshold = tableSize * loadFactor,因此如果可以提前知道hashMap的元素个数,那么直接指定大小和loadFactor=1可以有效避免resize。
树化
树化时机
1.容量>=64
2.链表长度>=8
树化过程
1.普通节点转化为树状节点(node->Treenode)
2.调用treeify进行树化
扩容
扩容时机
- size>=threshold(容量*loadFactor)
- 扩容大小:原来的2倍
原有节点移动
- 因为数组变大了,因此为了寻址,我们此时需要将各个槽位上的值分散到新的合适的槽位上
算法
因为我们原来的hash方式:(hash) & (n-1)
此时n = 2n 了,那么n-1意味着左边加了1位。(1111-> 11111)
那么,其实此时我们只要知道hash对应加的那一位(11111)上,是否是1,就知道需不需要移动了:
- 如果是1,意味着新的hash&(n-1)值大了,此时bin的位置是A+2^m;
- 如果是0,那么不用移动,和原来的相同。
而由于我们每个槽位上都是通过(hash & (n-1))得来的,因此在新的数组上,对应关系只能如下:
- 原来是A槽位的:hash&(n-1) = A.
- 扩容后:hash&(n-1) = A 或者A+2^m,只能是在这两个位置上,不会被分到和原来是其他bin里的数据一组的。
因此,在数组扩容后,我们只需要将每个bin上的数据,按规则分到A或者A+2^m的位置上即可,不会有互相串的可能性。
操作
- 新hash后的值不变的分到lo,
- 变的分到hi
搞定之后,直接接到新的数组上面就可以。
树node也是:
- 先执行相同的操作分为两段(其实树node中也保留了链表的next指针,因此可以很快地遍历)
- 然后,根据上面两段的计数,决定是以树的方式,还是以链表的方式,接到数组上
get
这个就没什么好讲的了,寻址->查找,和get方法中查找插入位置相同的