源码之Hashmap(算法设计)

1.在看HashMap 的底层的时候,会有个疑问,为什么数组长度为何总是2的n次方?
HashMap 在其构造函数 HashMap(int initialCapacity, float loadFactor) 中作了特别的处理,如下面的代码所示。当底层数组的length为2的n次方时, h&(length - 1) 就相当于对length取模,其效率要比直接取模高得多,这是HashMap在效率上的一个优化。

// HashMap 的容量必须是2的幂次方,超过 initialCapacity 的最小 2^n 
int capacity = 1;
while (capacity < initialCapacity)
    capacity <<= 1;  

HashMap 中的数据结构是一个数组链表,我们希望的是元素存放的越均匀越好。最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较Key,而且空间利用率最大。
那如何计算才会分布最均匀呢?

  • 使用 hash() 方法用于对Key的hashCode进行重新计算,以防止质量低下的hashCode()函数实现。由于hashMap的支撑数组长度总是 2 的倍数,通过右移可以使低位的数据尽量的不同,从而使Key的hash值的分布尽量均匀;
  • 使用 indexFor() 方法进行取余运算,以使Entry对象的插入位置尽量分布均匀(下文将专门对此阐述)。
    对于取余运算,我们首先想到的是:哈希值%length = bucketIndex。但当底层数组的length为2的n次方时, h&(length - 1) 就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。除此之外,HashMap 的底层数组长度总是2的n次方的主要原因是什么呢?
    这里,我们假设length分别为16(2^4) 和 15,h 分别为 5、6、7。
    源码之Hashmap(算法设计)_第1张图片
    我们可以看到,当n=15时,6和7的结果一样,即它们位于table的同一个桶中,也就是产生了碰撞,6、7就会在这个桶中形成链表,这样就会导致查询速度降低。诚然这里只分析三个数字不是很多,那么我们再看 h 分别取 0-15时的情况。
    源码之Hashmap(算法设计)_第2张图片
      从上面的图表中我们可以看到,当 length 为15时总共发生了8次碰撞,同时发现空间浪费非常大,因为在 1、3、5、7、9、11、13、15 这八处没有存放数据。这是因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。这样,空间的减少会导致碰撞几率的进一步增加,从而就会导致查询速度慢。

而当length为16时,length – 1 = 15, 即 1111,那么,在进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以,当 length=2^n 时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

因此,总的来说,HashMap 的底层数组长度总是2的n次方的原因有两个,即当 length=2^n 时:

  • 不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,空间利用率较高,查询速度也较快;
  • h&(length - 1) 就相当于对length取模,而且在速度、效率上比直接取模要快得多,即二者是等价不等效的,这是HashMap在速度和效率上的一个优化。

2.hash算法

获取put方法中,get方法中获取key的hash值的方法

//重新计算哈希值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//key如果是null 新hashcode是0 否则 计算新的hashcode
}

为什么要无符号右移16位后做异或运算
根据上面的说明我们做一个简单演练
源码之Hashmap(算法设计)_第3张图片
将h无符号右移16为相当于将高区16位移动到了低区的16位,再与原hashcode做异或运算,可以将高低位二进制特征混合起来

从上文可知高区的16位与原hashcode相比没有发生变化,低区的16位发生了变化

我们可知通过上面(h = key.hashCode()) ^ (h >>> 16)进行运算可以把高区与低区的二进制特征混合到低区,那么为什么要这么做呢?

我们都知道重新计算出的新哈希值在后面将会参与hashmap中数组槽位的计算,计算公式:(n - 1) & hash,假如这时数组槽位有16个,则槽位计算如下:
源码之Hashmap(算法设计)_第4张图片
仔细观察上文不难发现,高区的16位很有可能会被数组槽位数的二进制码锁屏蔽,如果我们不做刚才移位异或运算,那么在计算槽位时将丢失高区特征

也许你可能会说,即使丢失了高区特征不同hashcode也可以计算出不同的槽位来,但是细想当两个哈希码很接近时,那么这高区的一点点差异就可能导致一次哈希碰撞,所以这也是将性能做到极致的一种体现

使用异或运算的原因
异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向0靠拢,采用|运算计算出来的值会向1靠拢

你可能感兴趣的:(Java源码)