【Java集合篇】为什么HashMap的Cap是2^n,如何保证?

为什么HashMap的Cap是2^n,如何保证?

✔️目录

✔️ 为什么是2 ^ n ?

HashMap是通过 (table.length - 1) & (key.hashCode ^ (key.hashCode >> 16)) 定位 tablelndex 的。

为什么是用 & 而不是用 % 呢 ? 因 为 & 是基于内存的二进制直接运算,比转成十进制的取模快的多。又因为 X % 2^n = X & (2n - 1),可以把%运算转换为&运算。所以,hashMapcapacity 一定要是 2^n。这样,HashMap计算hash的速度才够快。

✔️为什么 X %2^n = X & (2^n - 1) ?

假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 -1 = 7,即0111。

此时X &(2^3 - 1)就相当于取X的2进制的最后三位数。

从2进制角度来看,X/8相当于 X >>3,即把X右移3位,此时得到了X/8的商,而被移掉的部分(后二位),则是X % 8,也就是余数。

上面的解释不知道你有没有看懂,没看懂的话其实也没关系,你只需要记住这个技巧就可以了。或者你可以找几个例子试一下。

如: 6%8 = 6,6&7= 6 10 %8 = 2,10&7 = 2

✔️如何保证

要想保证 HashMap 的容量始终是2^n次方,需要在Map初始化的时候,和扩容的时候分别保证。

java 复制代码
/**
*    当我们在实现HashMap时,为了保持其容量始终为2的幂,我们可以重写其resize方法,在每次扩容时调整容量为最接近的2的幂。
*/
public class MyHashMap<K, V> extends HashMap<K, V> {  
    private static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16, which is 2^4  
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;  
  
    @Override  
    protected void putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict, Node<K,V> node) {  
        // 在这里添加初始化时的逻辑  
        System.out.println("Putting key: " + key + ", value: " + value);  
        super.putVal(hash, key, value, onlyIfAbsent, evict, node);  
    }  
  
    @Override  
    protected Node<K,V>[] resize() {  
        // 获取当前容量  
        int oldCapacity = this.capacity;  
        // 计算新的容量,为最接近的2的幂  
        int newCapacity = Integer.highestPowerOfTwo(oldCapacity * 2);  
        // 调用父类的resize方法  
        return super.resize();  
    }  
}

✔️初始化时期保证

当我们通过 HashMap(int initialCapacity)设置初始容量的时候,HashMap并不一定会直接采用我们传入的数值,而是经过计算,得到一个新值,目的是提高hash的效率。(1 -> 1、3`-> 4、7 -> 8、9 -> 16)

在JDK 1.7和JDK 1.8中,HashMap初始化这个容量的时机不同。JDK 1.8中,在调用HashMap的构造函数定义HashMap的时候,就会进行容量的设定。而在JDK 1.7中,要等到第一次put操作时才进行这一操作。

看一下JDK是如何找到比传入的指定值大的第一个2的幂的:

java 复制代码
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;
	n |= n >>>32;
	n |= n >>>64;
	n |= n >>>128;
	n |= n >>>256;
	return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

上面的算法目的挺简单,就是: 根据用户传入的容量值(代码中的cap),通过计算,得到第一个比他大的2的幂并返回。

请关注上面的几个例子中,标记颜色字体部分的变化情况,或许你会发现些规律。

5->8、9->16、19->32、37->64 都是主要经过了两个阶段

Step 1,5->7

Step 2,7->8

Step 1,9->15

Step 2,15->16

Step 1,19->31

Step 2,31->32

对应到以上代码中,Step1:

java 复制代码
	n |= n >>>1;
	n |= n >>>2;
	n |= n >>>4;
	n |= n >>>8;
	n |= n >>>16;

对应到以上代码中,Step2:

java 复制代码
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

Step 2 比较简单,就是做一下极限值的判断,然后把Step 1得到的数值+1。

Step 1 怎么理解呢?其实是对一个二进制数依次无符号右移,然后与原值取或。其目的对于一个数字的二进制,从第一个不为0的位开始,把后面的所有位都设置成1。

因为cap是int类型的,所以最多需要右移16位即可获取其最大值

随便拿一个二进制数,套一遍上面的公式就发现其目的了:

java 复制代码
1100 1100 1100 >>>1 = 0110 0110 0110
1100 1100 1100 | 0110 0110 0110 = 1110 1110 1110
1110 1110 1110 >>>2 = 0111 1011 1011
1110 1110 1110 | 0011 1011 1011 = 1111 1111 1111
1111 1111 1111 >>>4 = 1111 1111 1111
1111 1111 1111 |  1111 1111 1111 = 1111 1111 1111

通过几次无符号右移和按位或运算,我们把1100 1100 1100转换成了1111 1111 1111,再把1111 1111 1111加1,就得到了1 0000 0000 0000,这就是大于1100 1100 1100的第一个2的幕。

好了,我们现在解释清楚了Step 1和Step 2的代码。就是可以把一个数转化成第一个比他自身大的2的次幂。

但是还有一种特殊情况套用以上公式不行,这些数字就是2的幂自身。如果cap=4套用公式的话。得到的会是 8,不过其实这个问题也被解决了,重点就在 int n = cap - 1; 这行代码中,HashMap会事先将用户给定的容量-1,这样就不会出现上述问题了。

总之,HashMap根据用户传入的初始化容量,利用无符号右移和按位或运算等方式计算出第一个大于该数的2的幕。

✔️扩容时期保证

除了初始化的时候会指定HashMap的容量,在进行扩容的时候,其容量也可能会改变。

HashMap有扩容机制,就是当达到扩容条件时会进行扩容。HashMap的扩容条件就是当HashMap中的元素个数 (size) 超过临界值 (threshold) 时就会自动扩容。

在HashMap中, threshold = loadFactor * capacity

loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,设置成0.75有一个好处,那就是0.75正好是3/4,而 capacity 又是2的幕。所以,两个数的乘积都是整数。

对于一个默认的HashMap来说,默认情况下,当其size大于12(16*0.75)时就会触发扩容。

下面是HashMap中的扩容方法(resize)中的一段:

java 复制代码
if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
	newThr = oldThr << 1: // double threshold
}

因为oldThr已经是2^n了, 所以oldThr无符号左移之后,是oldThr*2,自然也是2^n。至于后续HashMap是如何扩容的,请听下回分解~

相关推荐
芊寻(嵌入式)6 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
WaaTong7 分钟前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_743048448 分钟前
初识Java EE和Spring Boot
java·java-ee
AskHarries9 分钟前
Java字节码增强库ByteBuddy
java·后端
一颗松鼠15 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
有梦想的咸鱼_17 分钟前
go实现并发安全hashtable 拉链法
开发语言·golang·哈希算法
海阔天空_201322 分钟前
Python pyautogui库:自动化操作的强大工具
运维·开发语言·python·青少年编程·自动化
天下皆白_唯我独黑29 分钟前
php 使用qrcode制作二维码图片
开发语言·php
小灰灰__29 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭33 分钟前
Java中的动态代理
java·开发语言·aop·动态代理