在之前的文章 我分析过HashMap 初始化容量的问题 不清楚的可以看这个。
经过这篇文章 我们知道了 HashMap是什么时候 设置容量大小的,容量大小和容量的阀值 是怎么计算的,但是有的小伙伴 包括我 可能对一点比较好奇 为什么默认的容量是16 而且计算是自己容量的时候,最终计算出来的容量也是2的幂次方?可能 有的小伙伴知道 这个是为了 降低哈希碰撞率,那是为什么呢?那我们今天就来聊一聊
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
照顾下 没看之前的文章的小伙伴 还是回顾下
这个方法是Hashmap里面去计算初始容量需要用的 其目的就是获取一个大于当前传入的cap值的2的最小幂次方的数值。
看完这句话 感觉好拗口的 ,又是大于 又是小于的 比较懵逼,没事 那我们给出2个栗子:
我相信 看到了这个我相信 小伙伴们 一定知道了吧~
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
...
}
上面是我截取的 计算HashMap 位置的代码
我们知道 HashMap 里面计算卡槽的位置 是经过了这么几步:
首先 我们调用hash这个方法,这个方法 就是回去key的hascode值 然后和hashcode值的右移16位后 的值 做异或处理
为什么要这么处理呢?
首先我们知道异或【^】的逻辑运算的规则是 相同为0 不同为1 即:0 ^ 0=0,1 ^ 1=0,1 ^ 0=1,0 ^ 1=1
与【&】运算的规则 2者都为1 时候结果为1 否则为0 即:0&0=0, 1&1=1 ,1&0=0 ,0^1=0
顺便也会回顾下 或运算符【|】 规则是 只有有1 那结果就是1 即:0&0=0,1&1=1 ,1&0=1 ,0^1=1
记住下 上2个逻辑运算符 方法里面会涉及到
那我们再来回顾下 (h = key.hashCode()) ^ (h >>> 16) ,h >>> 16 这个操作就是 将二进制位往右移16位 空位的用0补足,知道了这个 我们来举个小demo 来看下 这个是要做什么,16为的花 我要写很长一段数组 那我就右移4位吧,举例说明下:
如果key的hashCode 的二进制值是:1011 0011 那么>>> 4的结果就是0000 1011 那么2者相【^】异或的结果是:
1011 0011
0000 1011
结果: 1011 1000
看到这样的结果是 高4位的数值 其实没有变化 低4位的数值 是有变化的,这样做的目的是把高4位的影响 降低到低4位中,保证hash后面hash值 在计算位置的时候 减少hash的冲突,具体 可以看下 英文的注释,翻译过来大概就是这个意思!
这个是计算位置的方法 hash 值就是上个方法中 传入过来的
如果这个是的hash值是 1011 1000 我们的n值就是16,那么就算结果将是这样的
hash: 1011 1000
n-1 : 0000 1111
结果: 0000 1000 10进制数值是:8
如果这个时候改变hash值是 1011 1100 那么就算结果将是这样的
hash: 1011 1100
n-1 : 0000 1111
结果: 0000 1100 10进制数值是:12
这个时候 我们看到 随着我们的hash值的不同 的到的index 也是不同的 ,这样能保证hash值 能均分的分配
但是如果这个是 我们的n长度不是2的幂次方,比如是10 ,那么计算结果是这样的
hash: 1011 1000
n-1 : 0000 1001
结果: 0000 1000 10进制数值是:8
这个时候 我们还是修改 hash值是 1011 1100 那么就算结果将是这样的
hash: 1011 1100
n-1 : 0000 1001
结果: 0000 1000 10进制数值是:8
这个时候 我们发现 虽然hash值修改了 但是 我们计算得到的index 还是相同的,就那上面这个例子 如果hash值是 1011 1000, 1011 1100, 1011 1110 计算得到的结果 都是 0000 1000 index 都是8 这样会提高Hash的冲突率,这显然不是我们想要的。
看到这里 你是否 知道了 为什么都是2的幂次方了么, 因为2的幂次方减掉1后, 二进制的值的 低位都是1,这样就保证了和Hash值 相与【&】后结果 低位都是被保留下来的 这样就可以避免不同的Hash值传入进来 被分配到 同一个卡槽位置 去存储。
通过以上的分析 你是否清楚了呢。不清楚的话 多看看 多想想 ,设计是如此的其妙!一个细节都是那么的优秀,值得我们去深究 和学习。
废话不多说 ,总结一下 hashMap中容量的数值都是2的幂次方 是为了降低hash的冲突,那位什么能降低hash的冲突呢,是因为2的幂次方-1 后的数组的二进制 的低位 全部都是1,这样保证了和hash值与【&】之后 能够完整的保留hash值的低位 这样就避免了不同的hash值过来被分配到一样的hash卡槽中!
总计完毕!!!!