为什么Map的大小必须是2的幂

环境:jdk1.8

构造函数

首先我们看下HashMap构造函数,以及默认容量DEFAULT_INITIAL_CAPACITY设置,指定初始化容量的构造函数中对初始化容量做了2的幂处理,例如:指定17,处理后会变成32(向上取幂)。默认容量16也是2的幂,并且注释中写明了必须为2的幂。

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
...
public HashMap(int initialCapacity, float loadFactor) {
    ...
    this.threshold = tableSizeFor(initialCapacity);
}

很多朋友可能会感觉到奇怪,为什么必须要是2的幂呢?我们继续看它的源码来挖掘原因

put

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

第一步是取key的hash值,我们知道一个hash算法的好坏主要看其hash后元素分布的均匀性,越是均匀,hash冲突也就是越少。我们看下hashmap如何实现的hash算法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. hashCode()函数是Object提供的native函数,调用系统函数返回一个内存地址转换而来的int值
  2. h ^ (h >>> 16),该函数什么意思呢?首先h无符号右移16位,刚好32位的一半,然后h与移位的结果做异或运算。异或算法也就是同假异真或不进行进位的加法。右移16位后高16位全部补0,与原高位16位异或结果不会改变原高16位。低16位与移位后的高16位异或运算

第二步其实就是将Object提供的hashCode返回值的低16位变成它的低16位与高16位异或运算后的值。这么做有什么好处呢?
如果低16位的两个位为00,高16位对应位置的两个位异或运算后两个位相同的概率是2/4(00或11),也就是有2/4的概率不同,异或后结果不是00或11的概率有2/4(01或10),显然对于一个两位的二进制01比00更均匀。所有场景如下表。可以看出异或运算后结果更加倾向均匀分布。正式我们期待的结果

两位长度二进制组合 异或运算后两个位数字相同的概率 异或运算后两个位数字不相同的概率
00 2/4 2/4
01 1/4 3/4
10 1/4 3/4
11 2/4 2/4

第二步putVal

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    ...
}

tab就是map的hash表,根据hash判断表中是否存在数据,也就是根据hash转换为hash表的下标,然后判断下标处是否有数据即可。hash值不能直接作为下标吗?为什么要转换?因为我们的hash表的大小默认是16,通常指定hash表的大小也不会是Integer最大值,因为我们的hash函数返回的是一个int值,那么就会存在下标越界问题,如何处理下标越界问题呢?直接使用hash值对hash表的大小取余数即可。那么我们看下HashMap如何处理的?显然不是直接用的取余算法,而是按位与运算:hash表大小-1 & hash值。

命题:X % (2^n) = [(2^n) - 1] & X

上面我们看到HashMap中的处理:(hash表大小-1) & hash值等同于取余运算
首先,我们回忆下2进制换算10进制的方法:
12的二进制表示:1100=12^3 + 12^2 + 02^1 + 02^0
取余数算法:
13%3=1:3+3+3+3+1
13%11=2:11+2
13%8=5:8+5
13%2=1:2+2+2+2+2+2+1

13转为2进制的另一种表示方法:12^3 + 12^2 + 02^1 + 12^0
对2取余,2转为2进制的另一种表示方法:12^1 + 020=1*20
对4取余,4转为2进制的另一种表示方法:12^2 + 02^1 + 020=0*21 + 12^0
对8取余,8转为2进制的另一种表示方法:12^3 + 02^2 + 02^1 + 020=1*22 + 02^1 + 12^0

写程序验证可以得出结论,即:X∈Integer,2^n∈Integer

结论

任意10进制数字对2的幂求模运算,等于10进制转为2进制后第(2^n) - 1右边的所有位转成10进制后的数字。
如何获取10进制转二进制后,第(2^n) - 1位的右边的所有位。没错就是对(2^n) - 1按位与运算。如下表:

2^n 13的二进制 [(2^n)-1]的二进制 按位与远算 余数
n=1 2 1101 0001 0001 1
n=2 4 1101 0011 0001 1
n=3 8 1101 0111 0101 5

性能比较

小于等于1千万次,模运算性能高于位运算
大于等于1亿次,模运算性能低于位运算

位运算耗时 模运算耗时 模运算耗时/位运算耗时 位运算耗时/模运算耗时
100000次位运算耗时 13 100000次模运算耗时 8 0.615384615 1.625
1000000次位运算耗时 86 1000000次模运算耗时 59 0.686046512 1.457627119
10000000次位运算耗时 628 10000000次模运算耗时 559 0.890127389 1.123434705
100000000次位运算耗时 5125 100000000次模运算耗时 5830 1.137560976 0.879073756
1000000000次位运算耗时 60795 1000000000次模运算耗时 70724 1.163319352 0.859609185

性能比较代码

package com.mytest;

import java.util.Random;

/**
 * @author 会灰翔的灰机
 * @date 2020/3/15
 */
public class BitAndModular {

    public static void main(String[] args) {
        bit();
        modular();
    }

    public static void bit() {
        int power = 10;
        int number = 10000;
        while(true) {
            number *= power;
            if (number <= 1000000000) {
                long start = System.currentTimeMillis();
                int result = 0;
                for (int j = 1; j < number; j++) {
                    result = (16777216 - 1) & new Object().hashCode();
                }
                long end = System.currentTimeMillis();
                System.out.println(String.format("%s次位运算耗时\t%s", number, (end - start)));
            } else {
                break;
            }
        }
    }

    public static void modular() {
        int power = 10;
        int number = 10000;
        for(;;) {
            number *= power;
            if (number <= 1000000000) {
                long start = System.currentTimeMillis();
                int result = 0;
                for (int j = 1; j < number; j++) {
                    result = new Object().hashCode() % 16777216;
                }
                long end = System.currentTimeMillis();
                System.out.println(String.format("%s次模运算耗时\t%s", number, (end - start)));
            } else {
                break;
            }
        }
    }
}
发布了91 篇原创文章 · 获赞 103 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/u010597819/article/details/104881525