为什么hashmap的容量必须是2的n次幂

 

要明白为什么是2的n次幂,这要从hashmap的hash方式说起,hashmap的容量期望就是用来均匀散列存放map中的元素。hashmap根据hash值把元素放到hashmap内部数组的一个位置上。

1、为什么hashmap的容量必须是2的n次幂??

我们不妨先看看hashCode的原理,以String为列,获取hashCode的方法源码

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

可以看出任何对象的hashCode方法返回一个对应的int类型的结果,所以hashCode的取值范围是-2^31 ~ (2^31-1),即-2147483648~2147483647。

(值得注意的是:两个对象equals相等,hashCode一定相等,hashCode相等,equals不一定相等,从上段代码hashCode源码中可以得出这个结论,会存在不同的数,计算出相同的hashCode)

当要存入map中的条目数为n(0 < n <= 1073741824)的时候,这n个数要散列分布到map数组中,那么map数组的长度为多少,才能使得元素在map数组最为呈现散列均匀分布,又不浪费空间呢?那么就是要求每个数通过某种算法,填入到数组表中的概率是相等的。这种算法最容易想到的就是取模运算。

我们联想下通过运算能不能达到类似效果呢,因为计算机本身就是通过位运算完成所有计算的,通过测试发现位运算比模运算快约27倍。怎么个位运算,使得n个元素能在有指定数组长度的情况下,得到散列分布索引值呢?

参考hashMap获取索引的算法源码

    public static int indexFor(int h, int length) {
        return h & (length-1);
    }

indexFor中的h是hashCode通过变换之后的值。怎么变换呢?为什么要变换呢?hashCode结果是一个32位的二进制数,如果直接用如此长的二进制数和目标length-1直接进行与运算结果为怎么样呢?答案是高位(也就是左边的位)会大量丢失。看看下面例子

如果两个数的hashCode分别是

11001110 11001101 11011101 00111110

11001110 11001111 11011101 00111110

只有其中第15位不一样(任何高位不一样都可以,假如我们以16位为划分,任何两个高16位不一样,低16位一样的数),这两个hashCode与length-1做与运算,当length<2的16次方时(1<<16 = 65536)得到两个hashCode & length-1 的结果一样,这样的两个数,却产生了相同的hash结果,于是hashMap想到了一种处理方式:将hashCode的高16位于低16位进行异或运算,其实吧用普通话说就是把高16位的特征放到低16位中,让低低16位同时拥有了高16的特征。这样,在与length-1做与运算时结果就不一样了。其计算过程如下:假如有hashCode a=11001110 11001111 11011101 00111110

int a =  11001110 11001111 11011101 00111110

a >>> 16 = 00000000 00000000 11001110 11001111

a ^ (a >>> 16)

11001110 11001111 11011101 00111110
00000000 00000000 11001110 11001111
=
11001110 11001111 00010011 11110001

用java代码表达 

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

hashCode结果是32位int 结果,将32位的结果做变换,可以从下面例子来研究下

int a =  11001110 11001111 11011101 00111110

a >>> 16 = 00000000 00000000 11001110 11001111

a ^ (a >>> 16)

11001110 11001111 11011101 00111110
00000000 00000000 11001110 11001111
=
11001110 11001111 00010011 11110001

在算法上可以看出,32位hashcode中保持高16位不变,高16与低16异或结果作为新的低16位。然后用hash得到的结果传入方法indexFor获取到hashMap的索引。

hash与n-1 做与运算,这样既在容量远小于1<<16(往往hashmap的容量远小于65535) 的情况下,hash & (n-1),在计算中只有低位((n-1)对应的二进制数相同的位数)参与&运算,计算效率高,同时也保证的hash的高16位参与了索引运算。这样得到的索引能呈较为理想的散列分布,在将条目放入hashMap中时,最大限度避免hash碰撞。

设hashMap容量为16,那么我们看下计算出来的索引值

11001110 11001111 00010011 11110001
&
00000000 00000000 00000000 00001111
=
00000000 00000000 00000000 00000001

索引=1,将该条目存入hash表标号为1的位置处

回到标题中的问题:为什么容量一定要是2的n次幂,只有当length=2的n次幂的时候,length-1的二进制表达 全部为1(15的二进制1111,31的二进制位11111),只有当length-1的全部位都1时,h & (length-1)的结果才能均匀散列在数组中。这时,取模和与运算两种运算才是等价不等效的。至此就解释完了为什么hashMap的容量必须是2的整数次幂。

2、hashmap怎么去计算容量呢?怎么确保程序员传入的值是2的n次幂呢,答案是不能保证。那就只能通过内部算法hashMa自己来实现保证。

先不考虑扩容的情况下,通过传入我们自定义的容量值来构造HashMap实例

    public HashMap(int initialCapacity) {
        // DEFAULT_LOAD_FACTOR = 0.75
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

怎么保证传入的容量一定2的整数次幂,可以从源码来看,通过下面方法等到保证,结果一定是2的整数次幂,结果是大于等于initialCapacity的最小2的整数次幂。

n |= n >>> 1,  符号 | 为或运算,n 与 n>>>1 进行或运算,然后赋值给n

    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容量是,初始化默认容量为16,当往HashMap中put时,会检查当前hash表中的条目数是否大于容量的0.75倍,如果满足大于容量的0.75,hashMap调用resize方法,新容量 = 原来容量 x 2,在resize时将原来的容量翻倍,然后把之前所有条目复制一遍放入扩容之后的hash表中,这是非常好性能的操作,所以在已知容量数量的情况下避免扩容的情况发生。


 

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值