Map 容量大小设计

by 董唯良 at 2016.7

背景

之前分析了下 Map 的源码,发现了这种底层源码的设计确实有独到之处,特别分享一下。

map 寻址:根据 key 的 hashCode 以及 Map 的长度计算出这个 key 所在的 index

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

这段代码中使用了按位取与的一个操作,而之前我所了解的根据 hashcode 的定位操作时用 hashcode % length 这种方式。

经过验证:这种按位与的结果与 hashcode % length 的结果是一致的,但是有一个前提——length 必须为 2 的幂次方。

map 如何保证自己的 length 为 2 的幂次方?

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();
}

从 HashMap 的构造函数可以看到,map 的容量大小并不是我们传入的 initialCapacity,而是刚刚大于 initialCapacity 并等于 2 的幂次方。

举例

假设数组长度分别为 15 和 16,key 的 hash 码分别为 8 和 9,那么 & 运算后的结果如下:

h & (table.length-1)   hash     table.length-1

8 & (15-1):           0100  &  1110           =     0100

9 & (15-1):           0101  &  1110           =     0100

------------------------------------------------------------------

8 & (16-1):           0100  &  1111           =     0100

9 & (16-1):           0101  &  1111           =     0101
  • 从上面的例子中可以看出:当 8、9 两个数和 (15-1)2=(1110) 进行与运算 & 的时候,产生了相同的结果,都为 0100,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8 和 9 会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到 8 或者 9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为 15 的时候,hash 值会与 (15-1)2=(1110) 进行与运算 &,那么最后一位永远是0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大。

  • 而当数组长度为 16 时,即为 2 的 n 次方时,2n-1 得到的二进制数的每个位上的值都为 1(比如 (24-1)2=1111),这使得在低位上 & 时,得到的和原 hash 的低位相同,就使得只有相同的 hash 值的两个值才会被放到数组中的同一个位置上形成链表。

结论

我们知道 &% 具有更高的效率,底层源码为了实现更高的效率还是比较煞费苦心的,也说明这些人确实很厉害。

Last updated