0
点赞
收藏
分享

微信扫一扫

【JDK源码系列】HashMap的hash函数


💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

【JDK源码系列】HashMap的hash函数_spring boot

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝

✨✨ 欢迎订阅本专栏 ✨✨


博客目录

  • 一.源码
  • 二.分析
  • 三.详细解析


一.源码

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

二.分析

  • key==null
  • key 等于 null 时,值为 0,所以 HashMap 的 key 可以为 null,
  • 对比 HashTable,如果 key 为 null,会抛出异常;HashTable 的 key 不可为 null
  • key!=null
  • 首先计算 HashCode 的值,再 HashCode 的值右移 16 位再和 HashCode 的值进行异或运算
  • 此过程就是扰动函数
  • 扰动函数原理,如果是小于 32 位,则右移 16 位后补零,进行异或运算后还是 0
  • 如果是 32 位的,则右移 16 位后,高位补 0,原来的高位变成了低位,进行亦或运算,增加随机,均匀分布

三.详细解析

HashMap 中的 hash 函数源码分析

【JDK源码系列】HashMap的hash函数_intellij idea_02

hash 方法的返回结果中是一句三目运算符,键 (key) 为 null 即返回 0,存在则返回后一句的内容

(h = key.hashCode()) ^ (h >>> 16)

JDK8 中 HashMap——hash 方法中的这段代码叫做 “扰动函数

我们来分析一下:

hashCode 是 Object 类中的一个方法,在子类中一般都会重写,而根据我们之前自己给出的程序,暂以 Integer 类型为例,我们来看一下 Integer 中 hashCode 方法的源码:

/**
   * Returns a hash code for this {@code Integer}.
   *
   * @return  a hash code value for this object, equal to the
   *          primitive {@code int} value represented by this
   *          {@code Integer} object.
   */
  @Override
  public int hashCode() {
      return Integer.hashCode(value);
  }

  /**
   * Returns a hash code for a {@code int} value; compatible with
   * {@code Integer.hashCode()}.
   *
   * @param value the value to hash
   * @since 1.8
   *
   * @return a hash code value for a {@code int} value.
   */
  public static int hashCode(int value) {
      return value;
  }

Integer 中 hashCode 方法的返回值就是这个数本身

注:整数的值因为与整数本身一样唯一,所以它是一个足够好的散列

所以,下面的 A、B 两个式子就是等价的

//注:key为 hash(Object key)参数

  A:(h = key.hashCode()) ^ (h >>> 16)

  B:key ^ (key >>> 16)

分析到这一步,我们的式子只剩下位运算了,先不急着算什么,我们先理清思路

HashSet 因为底层使用**哈希表(链表结合数组)**实现,存储时 key 通过一些运算后得出自己在数组中所处的位置。

我们在 hashCoe 方法中返回到了一个等同于本身值的散列值,但是考虑到 int 类型数据的范围:-2147483648~2147483647 ,着很显然,这些散列值不能直接使用,因为内存是没有办法放得下,一个 40 亿长度的数组的。所以它使用了对数组长度进行取模运算,得余后再作为其数组下标,indexFor( ) ——JDK7 中,就这样出现了,在 JDK8 中 indexFor()就消失了,而全部使用下面的语句代替,原理是一样的。

//JDK8中
  (tab.length - 1) & hash;

//JDK7中
  bucketIndex = indexFor(hash, table.length);

  static int indexFor(int h, int length) {
      return h & (length - 1);
  }

提一句,为什么取模运算时我们用 & 而不用 % 呢,因为位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快,这样就导致位运算 & 效率要比取模运算 % 高很多。

看到这里我们就知道了,存储时 key 需要通过hash 方法和**indexFor( )**运算,来确定自己的对应下标

(取模运算,应以 JDK8 为准,但为了称呼方便,还是按照 JDK7 的叫法来说,下面的例子均为此,特此提前声明)

但是先直接看与运算(&),好像又出现了一些问题,我们举个例子:

HashMap 中初始长度为 16,length - 1 = 15;其二进制表示为 00000000 00000000 00000000 00001111

而与运算计算方式为:遇 0 则 0,我们随便举一个 key 值

1111 1111 1010 0101 1111 0000 0011 1100
  &       0000 0000 0000 0000 0000 0000 0000 1111
  ----------------------------------------------------
          0000 0000 0000 0000 0000 0000 0000 1100

我们将这 32 位从中分开,左边 16 位称作高位,右边 16 位称作低位,可以看到经过&运算后 结果就是高位全部归 0,剩下了低位的最后四位。但是问题就来了,我们按照当前初始长度为默认的 16,HashCode 值为下图两个,可以看到,在不经过扰动计算时,只进行与(&)运算后 Index 值均为 12 这也就导致了哈希冲突

【JDK源码系列】HashMap的hash函数_数据_03

哈希冲突的简单理解:计划把一个对象插入到散列表(哈希表)中,但是发现这个位置已经被别的对象所占据了

例子中,两个不同的 HashCode 值却经过运算后,得到了相同的值,也就代表,他们都需要被放在下标为 2 的位置

一般来说,如果数据分布比较广泛,而且存储数据的数组长度比较大,那么哈希冲突就会比较少,否则很高。

但是,如果像上例中只取最后几位的时候,这可不是什么好事,即使我的数据分布很散乱,但是哈希冲突仍然会很严重。

别忘了,我们的扰动函数还在前面搁着呢,这个时候它就要发挥强大的作用了,还是使用上面两个发生了哈希冲突的数据,这一次我们加入扰动函数再进行与(&)运算

【JDK源码系列】HashMap的hash函数_哈希冲突_04

补充 :>>> 按位右移补零操作符,左操作数的值按右操作数指定的为主右移,移动得到的空位以零填充
^ 位异或运算,相同则 0,不同则 1

可以看到,本发生了哈希冲突的两组数据,经过扰动函数处理后,数值变得不再一样了,也就避免了冲突

其实在扰动函数中,将数据右位移 16 位,哈希码的高位和低位混合了起来,这也正解决了前面所讲 高位归 0,计算只依赖低位最后几位的情况, 这使得高位的一些特征也对低位产生了影响,使得低位的随机性加强,能更好的避免冲突

❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

【JDK源码系列】HashMap的hash函数_chrome插件_05


举报

相关推荐

0 条评论