一、取模运算和取余运算
- 取余运算,这个很好理解,我们经过多年的数学学习也知道,就是求余数,一个整数和另一个整数相除,得到它们的余数,就是我们说的取余
- 取模运算,通俗的来讲大多运算在计算机领域,取模运算其实就是两个二进制数字之间做与运算,它们最后得到的数字就是取模
- 我们举个简单的例子,有一个二进制数字0000 0001 1001 1101,1111 0101 1010 0011,这个两个数字做与运算,它们相同位置的数字,如果有一个数字出现1,那么计算后的数字的那个位置就是1,这两个数字与运算后的值为1111 0101 1011 1111这就是取模运算
二、hash函数在HashMap的源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 我们乍一看这个东西很懵,到底有什么用,讲这个源码之前,我们得先知道hashCode()函数是干嘛的?我们先看源码
@HotSpotIntrinsicCandidate
public native int hashCode();
- 这个就是hashCode函数的实现,从native关键字可以看出这是一个由底层C代码实现的函数,因此我们不必知道它是怎么实现,我们只需要知道,它就是返回一个int类型的数字,这个数字就相当于对象的身份证,指纹,因此可以利用这个“身份证”来进行归类,我们知道int值域在-2147483648 到 2147483648,这个范围有40多亿,因此想要相同也不容易,但是我们的HashMap底层数据结构是一个数组啊,用于存储这些节点,初始大小为16,因此按照我们常规做法就是,用这个“身份证”和数组大小(也就是16)做取余运算,这样我们就是把一个很大的数字映射成一个16以内的整数。然后我们就可以把这个节点存储到这个数组的某一个位置了。
三、hashMap中映射的解决方案
- 这个解决方案其实很好,但是我们还没有其他的解决方案呢?
- 其实是有的,在hashmap中的做映射,使用是取模运算而不是取余运算,这是为啥?
- 因为取余运算的效率远高于取余运算,我们知道任何数据存储在计算机内部都是以二进制形式存储的,因为两个int整数进行取模运算的时候,做几次比较立即就得出了,但是取余就要运算很长
- hashMap内部使用取模,比如我们一个对象的“身份证”是
- 1010 0111 0100 1010 1111 1001 0010 1010
- 我们数组大小是16,16的二进制是0000 0000 0000 0000 0000 0000 0001 0000
- 我们减一,那就是 0000 0000 0000 0000 0000 0000 0000 1111,用这个数字和上面那个“身份证”取模得到0000 * 7个,最后四位就是1010,多好啊!把一个数字映射成了一个16以内的数字,我们看看源码是不是这个方法
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);
- 可以看见正好n-1就是16-1=15这样2进制最后4位都是1,就是模取出来了
- 这也正好解释了为什么扩容都是*2了,因为-1之后就是后面几位数字都是1了,便于好取模,求出映射
- 我们看这里h&(h>>16),其实就是身份证和身份证往右移动16位之后取模,我们知道一个int值类型就是一个32位的二进制,这样其实就是把前16和后16位进行取模,这样就把一个增大了随机性,因为你看原来的方法只取了后面4位,前面的28位都没啥用,用这个方法之后,再取后4位就是把前面的16位也拿来了
- 总结:hash函数就是为了是对象的hash值增加随机性,它集合前16位和后16的特征进行计算,这样再被取模之后,会让对象在HashMap中分布更均匀。
四、源码:
- 欢迎关注微信公众号:傅里叶变换,个人账号,仅用于技术交流