HashMap与HashTable的哈希算法——JDK1.9源码阅读总结

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对于元素在哈希表中的坐标算法是:

  1. 将对象自身的哈希值key.hashCode()变为正数:hash & 0x7FFFFFFF
  2. 将上面得到的哈希值对表长取余,映射到哈希表中去。

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’

《HashMap与HashTable的哈希算法——JDK1.9源码阅读总结》
(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

总结

  1. 对数学规律的恰当应用可以优化代码的运行效率
  2. 位运算在JDK中运用的十分广泛,如上面讲解的使用位“与”运算替代求模。这种替代的内在原因是位运算比数学运算快很多。优化都在细节处。

我对JDK源码的阅读和中文注释都已经同步到Github,欢迎英语阅读困难户前往查看:)
链接是:https://github.com/Dodozhou/JDK,喜欢的话别忘了star哦。

    原文作者:哈希算法
    原文地址: https://blog.csdn.net/zhoucheng05_13/article/details/79815803
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞