JDK中HashMap的两个细节

背景

HashMap是面试中经常问到的主题,Java中级以上面试保守估计70%以上都会问。之所以这个主题这么经典,是因为它具备作为面试题很重要的一个特点:良好的区分度。

就像高考题一样,一道好的高考题不是说它有多难或者多简单,重要的是能作为一道筛子,过滤不同层次水平的考生。在我看来,HashMap这个筛子的过滤条件有:

  1. 是否阅读过源代码;
  2. 是否能读懂源代码;
  3. 是否有自己的思考。

没读过源代码的人,直接过滤掉;没读懂源代码的人,直接过滤掉;你要是高手,这个看似简单的问题也是可以答出花来的。

问题

HashMap的基本结构是"数组+链表",其中每个元素是保存key,value的Entry节点,你也可以说它是一个Node。这个问题,读过源码的人都知道。我们今天就来说说经常问到的两个问题:

  1. 为什么HashMap的内部结构中,capacity必须是 ?
  2. 为什么负载因子默认会选择0.75?

为了说明以上两个问题,我们先来看几点预备知识,后面会用得到。

预备知识

  • 位运算
    • 的位表示为,即一个1后面n个0。
    • 的位表示为,即n个1。
  • 模运算
    • 对任意m位二进制数X和(一个1后面n个0)进行X % Y的模运算具有以下特殊性质
      • 它相当于把X的二进制表示从n位开始分为高位H和低位L两部分(H包含m-n位,L包含n位)
      • 大概就是这样:
      • 高m-n位H等于X / Y
      • 低n位L等于X % Y
    • 所以对任意X取的模等价于,取X二进制表示的低n位L(用n个低位为1的掩码对X进行&运算)
      • X % Y == X & (Y - 1)
      • 前提条件就是Y ==
  • 效率的比较
    • 在许多古老的微处理器上,位运算比加减法略快,比乘法要快很多
    • 在现代处理器架构上,位运算和加法相同,但仍快于乘法。
    • 模运算/加法运算通常在一个时钟周期内完成,乘法和模运算这些运算需要多个时钟周期。
  • 浮点数的表示
    • 浮点数的表示法中,高m位用于表示整数部分,低n位用于表示小数部分。
    • 大概就是这个样子:
    • 在上式中逗号","为界,左边的m位每一位的权重是,右边的n位每一位的权重是
    • 所以浮点数的小数部分,能够精确表示的是,,,…以及它们组合而成的数字,比如0.75,1.625,1.5都可以精确表示,但0.8,0.3这样的数字本身就不能用浮点数精确表示

解答

具备了以上的知识储备后,前面提到的两个问题就迎刃而解了:

  • 第一个问题:为什么HashMap的内部结构中,capacity必须是 ?

    • 因为HashMap的hash算法的实现是

    • final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                         boolean evict) {
              Node[] tab; Node p; int n, i;
              if ((tab = table) == null || (n = tab.length) == 0)
                  n = (tab = resize()).length;
              if ((p = tab[i = (n - 1) & hash]) == null)
                  tab[i] = newNode(hash, key, value, null);
              else {
                  ... ...
              }
              ++modCount;
              if (++size > threshold)
                  resize();
              afterNodeInsertion(evict);
              return null;
          }
      
    • 第二个if条件里的tab[i = (n - 1) & hash],就是hash & (n - 1) <==> hash % n

    • 说白了HashMap的hash算法采用的是取模运算(n == length == capacity)

    • 前提条件是,换句话说,如果不满足capacity == ,取模运算的实现就是错误的

    • 取模运算的优点有

      • 散列均匀,假设输入是随机均匀的,那么输出就是随机均匀的
      • 通过转化为位运算,具有极快的运算速度,对于一个计算频率很高的方法,有不错的性能
  • 第二个问题:为什么负载因子默认会选择0.75?
    • 负载因子过大,比如是1.0,那么必须要size == capacity * 1.0才扩容,这个时候有多少空着的格子,就有多少个发生hash碰撞的节点。
    • hash算法的最好情况是没有碰撞,要想尽量少碰撞达到尽量高的效率,就要降低负载因子
    • 负载因子过小,比如是0.0,那么size == capacity * 0.0的时候就要扩容,即空map就要求扩容,整个空间都浪费了,那么要减少浪费提高空间利用率,就要提高负载因子
    • 一方面需要降低负载因子,另一方面需要提高负载因子;取一个不大不小折中的数字就是必然的选择
    • 0.75是性能和空间利用率折中的一种选择
    • 另一方面,负载因子的数据类型是float,0.75是一个可以用float精确表示的数,计算过程中不会因为丢失精度产生误差。当然这只是个次要因素,数值的大小才是关键,但这其中一定有精确不丢失精度的考量

你可能感兴趣的:(JDK中HashMap的两个细节)