为啥初始化HashMap需要指定容器初始大小?

107 阅读4分钟

我正在参与掘金创作者训练营第4期(带链接:juejin.cn/post/706419…

有时候当我们写完代码 检查代码时是不是会发现这样的提示?

image.png

于是我们这样修改下

image.png

是不是已经完全没有问题了?

写了2和不写真的会有很大的差别吗?

带着这俩问题我又发现了以下提示信息...

image.png

到这里结束?当然没有,我们强大的好奇心必然会驱使我们发现为啥是16 而不是2、4或者8?

于是我们可以查看下HashMap源码:

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

可以发现有个tableSizeFor方法

/**
 * Returns a power of two size for the given target capacity.
 */
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;
}

乍一看,我并不知道我在哪?我在干啥? 于是想到了个笨的方法来执行下这个方法

//测试tableSizeFor方法 这边我把HashMap类中的静态方法放在了本地类里面
Map data = new HashMap<>();
for (int i = 0; i <30 ; i++) {
    data.put(i,tableSizeFor(i));
}
System.out.println(JSON.toJSONString(data));

执行结果如下:

{
    "0": 1,
    "1": 1,
    "2": 2,
    "3": 4,
    "4": 4,
    "5": 8,
    "6": 8,
    "7": 8,
    "8": 8,
    "9": 16,
    "10": 16,
    "11": 16,
    "12": 16,
    "13": 16,
    "14": 16,
    "15": 16,
    "16": 16,
    "17": 32,
    "18": 32,
    "19": 32,
    "20": 32,
    "21": 32,
    "22": 32,
    "23": 32,
    "24": 32,
    "25": 32,
    "26": 32,
    "27": 32,
    "28": 32,
    "29": 32
}

是不是突然了 返回的结果就是比当前值最接近(>=)的2的n次幂 以上只是总结规律,并不能说我们已经读懂了那段代码

介绍下 |= 运算符

//举例: 以下是完全等价的
a |= b
a = a | b

运算逻辑:

int a = 5; // 用8位2进制表示为  0000 0101
int b = 13;// 用8位2进制表示为  0000 1101
a |= b;   // 用8位2进制表示为   0000 1101  a = 13

>>> 无符号右移。无论是正数还是负数,高位通通补0

int i = 3 >>> 1;  //结果为 1
// 3 表示为2进制数为:0011 往右位移1位高位补0 得到0001 所以结果为1

代码具体分析


static final int tableSizeFor(int cap) {
    int n = cap - 1;  //假设n为0100000000000001 减一之后为 0100000000000000
    n |= n >>> 1;     位移1位置 得到    0010000000000000  |=之后  为 0110000000000000
    n |= n >>> 2;     位移2位置 得到    0001100000000000  |=之后  为 0111100000000000
    n |= n >>> 4;     位移4位置 得到    0000011110000000  |=之后  为 0111111110000000
    n |= n >>> 8;     位移8位置 得到    0000000001111111  |=之后  为 0111111111111111
    n |= n >>> 16;   位移16位置 得到    0000000000000000  |=之后  为 0111111111111111
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个过程其实就是往低位的0位上补1 而第一步的减1操作 是为了防止当前传的cap值正好是2的幂次方 举例:

static final int tableSizeFor(int cap) {
    int n = cap;      //假设n为8  2进制表示为 1000 这边不减1
    n |= n >>> 1;     位移1位置 得到    0100  |=之后  为 1100
    n |= n >>> 2;     位移2位置 得到    0011  |=之后  为 1111
    n |= n >>> 4;     位移4位置 得到    0000  |=之后  为 1111
    n |= n >>> 8;     位移8位置 得到    00000000  |=之后  为 00001111
    n |= n >>> 16;   位移16位置 得到    0000000000000000  |=之后  为 0000000000001111
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    //返回 n+1 即:0000000000001111 加1  等于 0000000000010000 
    //10进制则表示为16 结果显然不是预期的
}

到这里 其实我们就明白了 这是为了防止最后n+1的时候升位 所以在n开始的时候 先减1

好了这样我们就知道了new HashMap(n) 的n需要放什么值了。

至于为什么要初始化这个值,简单点说就是为了性能。

image.png

注意:本次内容基于Jdk1.8 感谢阅读~