HashMap相关问题速记

164 阅读4分钟

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方法中查找插入位置相同的