初始容量与加载因子

阅读HashMap可以得知其默认的初始容量为16,默认的加载因子也为0.75.

根据Hash表的特性我们可以得知,容量必须为2的N次幂。想知道这个原因,我们就得知道HashMap是如何通过hashCode获取下标的

通过阅读源码可以知道,hashcode是通过位运算得到key的下标的,以下是hash的扰动函数与取模函数。

/**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     *
     * java8中的散列优化函数
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

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

获取下标我们分为以下三步

  • 1、获取hashCode

这个通过Object自带的方法获取(底层通过C/C++)

  • 2、扰乱hashCode增加随机性

通过对key的高16位与低16位异或操作得到hash值。通过混淆hash码的高位与低位增大随机性,防止诸如等差数列等碰撞程度非常大的特例的影响。

  • 3、取模运算

这个扰动函数结束后,得到了hash值。但是这个hash值得范围太大(-2^31^~2^31^),这么大范围的值做下标,内存是不够的。因而,我们还需对这个hash值进行取模才有实际意义。

初始容量是2^n^的原因:

return h & (length-1)这个语句就是我们初始容量必须2的n次幂的原因——如果不是n次幂的长度,我们使用hash值与容量-1进行与操作,肯定是有一些值取不到的。如下图:

hash

如果我们的长度是10,化为二进制是1010

那么对于最后一位与倒数第三位是取不到的。也就是0100(8)、0001(1)、0101(9)等下标是不会出现在HashMap中的。这是对内存的浪费

加载因子:表示Hash表元素的填满程度,如果达到条件需要自动扩容。那我们选什么值作为加载因子比较合适呢?如果加载因子太小,自动扩容的次数就多,对空间的消耗会大;如果加载因子太大,冲突的机会就会变大,查找的成本会提升,性能会下降。因此必须找到一个“空间”与“时间”的平衡点。

接下来让我们看看源码:

    /**
     * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
     **/

关键字:Poisson_distribution泊松分布

HashMap的结构是主干数组加链表。

通过对理想情况的研究,对随机hashCode,在加载因子为0.75的情况下,节点出现在hash表中遵循参数为0.5的泊松分布,也就是

$$ P(X=k)=\frac{e^{0.5}0.5^k}{k!} $$

一个链表长度达到8个元素时,概率为0.00000006,近似看成不可能事件。

当加载因子0.75时,$length \geq size\times0.75$就扩容,理想链表长度和概率结果为

0:    0.60653066
1:    0.30326533
2:    0.07581633
3:    0.01263606
4:    0.00157952
5:    0.00015795
6:    0.00001316
7:    0.00000094
8:    0.00000006

经过研究测试是加载因子0.75我们能接受的.原则上加载因子选取0.7-0.8。加载因子选择其他也不过是链表长度和概率有所不同,对特殊模型可以选择不同的取模算法、加载因子、初始容量制作定制HashMap。计算机从来没有什么绝对,只有对特定业务场景的最优选择。

如果对统计学有更深入的研究可以在往深考虑,对于一般的计算机专业的同学在有了基础的概念和理解就此打住比较好。

小声逼逼:一定要学好数学啊哭,大一学离散与概率学成天摸鱼划水,也就把作业写完了没有深入研究。想不到仅仅一年后报应就来了。整理一年前的笔记竟然在泊松分布懵逼了半天...


孑立
27 声望1 粉丝

在校读书的一个蒟蒻。未来方向是后端开发,业余学了一点python与c++QT


下一篇 »
原地Hash