Jdk_HashMap 源码 —— hash(Object)

Jdk 源码

HashMap 的源码是在面试中考的算是比较多的,其中有很多高性能的经典写法,也值得多学习学习。

本文是本人在阅读和学习源码的过程中的笔记(不是教程),如有错误欢迎指正。

Jdk Version : 8
System : windows

HashMap 之 hash(Object)

HashMap 的源码内容很多,知识点也特别的多,一篇文章肯定写不完。这里只讨论下 hash(Object) 这个方法的实现,其他的部分参考其他文章。

在 jdk8 中,向 HashMap 中存值的啥时候会调用 put(Key , Value) ,使用案例如下:

//                   key       value
new HashMap<>().put("zhangsan",new Person());

HashMap 的源码如下:

// jdk8 java.util.HashMap 源码第 611-613 行
public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

源码中,在 putVal() 之前,会先调用一下 hash(Object) ,他的传参是 key,此方法的作用是根据 key 值,生成一个数,以此来确认这对要储存的 key-value 放在数组的哪个位置(HashMap的底层数据结构:参考2),并且尽量要让不同的 key 生成的数据重复的概率小,而且不会相差范围太大。

其源码如下:

// jdk8 java.util.HashMap 源码第 337-340行
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

源码中 key == null 的时候返回 0 ,由此可见 HashMp 的 key 是可以为 null 的。

上面的源码其实挺精简的,我们先把它拆分一下:

static final int hash(Object key) {
    int h;
    if(key == null){
      h = 0;
    }
    else{
      // 第一步
      int h1 = key.hashCode();
      // 第二步
      int h2 = h1 >>> 16;
      // 第三步
      h = h1^h2;
    }
    return h;
}
  • 第一步:

第一步调用的是一个继承自 Object 的本地方法 public native int hashCode(); 此方法会根据对象生成一个 int 类型的 hash 值,而且同一个对象在同一个环境上每次生成的也都相同。这个方法在 jdk8 上是用 c/c++ 实现的,所以在不同的平台上可能实现的方法也有所不同(一般会根据线程或者对象的内存地址之类的信息来生成)。

public static void main(String[] args) {
    Object o = new Object();
    System.out.println(o.hashCode()); 
    // 输出:1915503092
    System.out.println(o.hashCode()); 
    // 输出:1915503092
}

理论上,得到的 hash 值范围是 -2147483648 到 2147483647 正负加起来有40多亿个这么多,所以不同的两个对象生成同一个 hash 值概率是很小很小的。

但是,如果此时直接将生成的这个 hash 值作为 HashMap 数组的下标的话,那么数组的大小就要40多亿,这显然是不行的(HashMap中数组的初始大小才 16 )。

于是,下面就要对这个这数进行处理,在尽量不损失所有信息的情况下,将这个数的范围缩小。

  • 第二步:

假设我们上面生成的 hash 值是 4294963434 ,它的的二进制是 1111 1111 1111 1111 1111 0000 1110 1010

将这个的二进制进行“无符号右移” 16 位,得到 0000 0000 0000 0000 1111 1111 1111 1111 ,这样我们得到的这个值就是将 hash 值的高 16 位的信息移动到低 16 位上。

如下图:

  • 第三步

将 h1 与 h2 做“异或”操作 h1 ^ h2。相当于现在低 16 位上即包含了原来的高 16 位信息又包含了原来低 16 位信息。这样做混合了高低位的数据,就将它的随机性尽可能的缩小到 16 位,我们只要用到 hash 的低 16 位就可以了。如下:

但是这个 h 依然很大,还是不能能够直接作为数组的下标。

  • 再次处理 hash 值

别急,这里只看了 hash(Object key) 这个方法,我们回到 put 方法的源码。

// jdk8 java.util.HashMap 源码第 611-613 行
public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

进到 putval 这个方法里。

// jdk8 java.util.HashMap 源码第 625-666行
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);
    else {

        // 此处省略部分源码...

    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  • tab:就是储存数据的额数组。
  • n:是数组的大小。

上面的源码中,对计算得来的 hash 值进行了一次计算 i = (n - 1) & hash。计算得到的 i 也就是 HashMap 中的数组下标了。

HashMap 的数组初始化大小是 16,16 - 1 = 15 ,换成二进制就是 0000 0000 0000 0000 0000 0000 0000 1111,所以计算“与”操作,就只保留了 hash 的最后四位,得到 5 就是数组的下标。

数组的长度 n 。 n - 1 与 hash 做“与”操作,也就把高位都归0了,结果的取值范围也刚好就是 0 到 n 。这个前提是 n 是 2 的整次幂。

这也解释了为什么很多面试都喜欢问的问题:“为什么HashMap 的数组长度要取整数幂?”

这样,随着 HashMap 的数组变大,碰撞的 key 值的冲突可能性理论上应该是越来越低的,同时在数组很小的时候,碰撞的概率也不是很大(因为地位混合了高位的随信息)。

至此。

参考

1、JDK源码中HashMap的hash方法原理是什么?(这个大佬写的是真的好)。
2、HashMap底层数据结构(数组+链表+红黑树)

作者:chenghd

出处:https://www.cnblogs.com/chenghd/p/16833710.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   CHENGHD  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
more_horiz
keyboard_arrow_up light_mode palette
选择主题
点击右上角即可分享
微信分享提示

目录导航