java源码分析(2)-为什么HashMap容量(capacity)总是2的n次幂?

81 阅读3分钟

先看hashMap的get和put方法

  • get

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 先获取table,table就是用来存储map所有的桶的。如果table为空,退出
        // 获取n,桶数目,如果为0,退出
        // 最后通过table[(n-1) & hash]获取key所在桶的第一个node。
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 下面先不看
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    
  • put

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;n
        // 这里是给tab赋为table,以及获取table的长度n。t
        // 如果table未初始化,就先进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // i表示的是该key所在桶位置
        // i = (n - 1) & hash 就是用来确定该key所在桶的下标的
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 下面先不看
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

分析

看上面注释的位置,我们会发现HashMap是通过hash&(n-1)来确定key在哪个桶的,n为桶的数目。

如果按照一般思路,我们要让hash映射到一个桶上,需要计算出一个桶的索引index,这个index取值范围是[0, n),n为桶的数目。那么我们可以用hash % n 计算得出,因为对n取余数的结果一定在[0,n)范围内

但是java有自己的想法。因为如果当n 为2的整数次幂的话,hash & (n-1) = hash % n 的。java用&运算来取代了%运算。因为&运算比%要来得快。至于为什么会有这个关系的原因如下:

众所周知,2的整数次幂的二进制只有一个1,其余都是0。

  • 比如 2^0 = (1)2, 2^1 = (10)2, 2^2=(100)2, 2^3 =(1000)2,2^10 = (10000000000)2

那么2次幂 - 1的话,最高一位变成了0,后面的0全都变为1

  • 比如 2^0 - 1 = (0)2, 2^1 - 1 = 01, 2^2 - 1 = (011)2, 2&3 - 1 = 0111, 2^10 - 1 = (01111111111)2

用高位以上全为0,后面全为1的数和hash做&运算,得到的数一定在[0, n)之间,

  • 假设n=2^10,那么(11111111111011010)2 & (2&10 - 1)如下
  11111111111011010

& 00000001111111111

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

000000001111011010

00000000111011010 是小于2^10次方的(0000000010000000000)。这个数也是hash % n的值

所以,结论就是,hashMap为了提高效率,在确定key属于哪个桶时,用hash & (n -1) 来取代 hash % n 。要使这个式子成立,n必须为2的整数次幂