HashTable中的哈希算法
下面是HashTable源码中的put方法:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; //注意这里
//下面的代码省略,注意上面一行
}
注意上面注释标注的地方:
int index = (hash & 0x7FFFFFFF) % tab.length; //注意这里
HashTable对于元素在哈希表中的坐标算法是:
- 将对象自身的哈希值key.hashCode()变为正数:hash & 0x7FFFFFFF
- 将上面得到的哈希值对表长取余,映射到哈希表中去。
HashMap中的哈希算法
HashMap中哈希算法比HashTable中的稍微复杂一点。总体可以分为两步:
一、重新计算key本身的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
上面代码中,首先是一个三目运算符,判断key是不是等于null,等于null,则返回0作为哈希值。否则,运算(h = key.hashCode()) ^ (h >>> 16),将key的哈希值的高位与低位异或的结果作为低位,改为不变。
‘>>>’是无符号右移操作,高位补0.
为什么要这么做呢?下面这一点讲了过后我们就明白了
二、哈希坐标的计算
同样以put方法为例
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)
//下面的代码省略,注意上面一行
从最后一行我们可以看出,HashMap的哈希坐标计算方法是: (n – 1) & hash,其中hash就是我们第一点讲的改进哈希码。
HashMap为什么要使用改进的hash码?
举例分析如下,假设key的原始哈希值是’1111 1111 1111 1111 1111 0000 1110 1010’
(ps:图片来自:https://blog.csdn.net/john_520/article/details/57415084)
我们注意到,在上面这种哈希表长度较小的情况下,哈希码只有低4位与表的长度进行了关联性计算。这会造成哈希码的不充分使用,从而更容易引起哈希冲突。为了充分利用哈希码的高位,HashMap通过(h = key.hashCode()) ^ (h >>> 16)运算,将高位与低位异或,使得即使在表长较小的情况下,高位也能参与计算,使得冲突的概率减小了。
HashMap为什么要使用 (n – 1) & hash ,而不是HashTable中求模的方式来计算哈希坐标呢?
我在Stack Overflow上找到了一个解答:
The "canonical solution" is to take the (positive) modulo of
the hash with the length of the array, this code uses the fact
that the array has a power-of-two length to replace an
pretty well) with a cheap bitwise AND.
意思是说:“规范的解决方法是将哈希值与表长取模,而这个方式((n – 1) & hash )充分利用了HashMap的表长是2的整数次幂的事实,使用效率较高的位与运算(取模的高度优化)来替代昂贵的取模运算。”
实际上这两种方式的实质是一样的,它只是利用了这样一个事实:在n是2的幂的情况下,(n – 1) & hash 等同于 hash%h。
在n是2的幂的情况下,为什么 (n – 1) & hash等同于 hash%h?
我们以10为例,演示如下:
hash = 10
hash % n = 10 % 8 = 2
(n - 1) & hash = 7 & 10 = 0 1 1 1 & 1 0 1 0 = 0 0 1 0 = 2
可以看到,两种方式的计算结果是相同的(这不是巧合),这实际上是一个数学规律。
HashMap是怎么使得表长始终为2的整数次幂的?
在源码中有这样一个方法:
/** * Returns a power of two size for the given target capacity. * 下面操作的目的是求出该数字的下一个2的整数次幂的数, * 如65的下一个是128,233的下一个是256 * 具体原理见下面链接 * https://zhidao.baidu.com/question/291266003.html */
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;
}
这个方法的作用如注释所说,是求大于cap的最小2的整数次幂。在用户指定的初始容量不是2的幂时,HashMap会调用该方法将其变得符合要求。此后,每次扩容时是这样的:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//将老容量扩大2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
直接使用oldCap<<1来将容量扩大为原来的2倍,即乘以21。
总结
- 对数学规律的恰当应用可以优化代码的运行效率
- 位运算在JDK中运用的十分广泛,如上面讲解的使用位“与”运算替代求模。这种替代的内在原因是位运算比数学运算快很多。优化都在细节处。
我对JDK源码的阅读和中文注释都已经同步到Github,欢迎英语阅读困难户前往查看:)
链接是:https://github.com/Dodozhou/JDK,喜欢的话别忘了star哦。