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 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!