Java源码系列2——HashMap
2020/2/24 17:03:19
本文主要是介绍Java源码系列2——HashMap,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
HashMap
的源码很多也很复杂,本文只是摘取简单常用的部分代码进行分析。能力有限,欢迎指正。
HASH 值的计算
前置知识——位运算
按位异或操作符^
:1^1=0, 0^0=0, 1^0=0, 值相同为0,值不同为1。按位异或就是对二进制中的每一位进行异或运算。
1111 0000 1111 1110 ^ 1111 1111 0000 1111 ______________________ 0000 1111 1111 0001 复制代码
按位右移补零操作符>>>
:左操作数按右操作数指定的位数右移,移动得到的空位以零填充。
1110 1101 1001 1111 >>> 4 ___________________________ 0000 1110 1101 1001 复制代码
扰动函数
为什么要做扰动?
理论上哈希值是一个int
类型,如果直接拿哈希值做下标的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。这么大的数组,内存是存不下的,所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。
因为只取最后几位,所以哈希碰撞的可能性大大增加,这时候扰动函数的价值就来了。
扰动计算
先调用hashCode()
方法得出hash值,再进行扰动操作。
右位移16位,正好是32bit的一半(int
是32位的),自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也变相保留下来。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 复制代码
取模,计算出下标
在计算下标的时候,让列表长度对哈希值做取模操作,让计算出来的哈希值在列表范围内,n 为list
长度
i = (n - 1) & hash 复制代码
为什么HashMap
的数组长度要取2的整次幂
因为这样(数组长度 - 1)正好相当于一个“低位掩码”。&
操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组的下标访问。以初始长度16为例,16-1=15,2进制表示是0000 1111。和某散列值做&
操作如下:
1010 0011 0110 1111 0101 & 0000 0000 0000 0000 1111 ____________________________ 0000 0000 0000 0000 0101 复制代码
是什么存入了 table
HashMap
存入table
的值并不只有value,而是构造成一个 Node 对象实例存入 table。
Node对象里有:hash, key, value, next(哈希冲突时的链表)
理论最大容量
int MAXIMUM_CAPACITY = 1 << 30;
2的30次方
负载因子
负载因子是用来计算负载容量(所能容纳的最大Node个数)的,当前list长度 length,负载因子 loadFactor
负载容量计算公式为:
threshold = length * loadFactor
默认负载因子为 0.75。也就是说,当Node个数达到当前list长度的75%时,就要进行扩容,否则会增加哈希碰撞的可能性。负载因子的作用是在空间和时间效率上取得一个平衡。
float DEFAULT_LOAD_FACTOR = 0.75f
扩容做了哪些操作
- 创建一个新的Entry空数组,长度是原数组的2倍。 当Node个数超过负载容量时,进行扩容。
old << 1
左移一位相当于 old * 2。
-
重新Hash
遍历原Entry数组,把所有的Entry重新Hash到新数组中。
为什么要重新hash?因为长度扩大以后,hash值也随之改变(数组下标的计算是数组长度对hashcode进行取模)。
这样就可以把原先哈希冲突的链表拉平,使数组变得稀疏。
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; } // 原有容量左移一位,相当于 oldCap * 2 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 负载容量也扩大一倍 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 负载容量为0,根据数组大小和负载因子计算出来 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 遍历数组中所有元素,重新进行hash if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 删除旧索引位置的值 oldTab[j] = null; if (e.next == null) // 给新的索引位置赋值 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 优化链表 // 把原有链表拆成两个链表 // 链表1存放在低位(原索引位置) Node<K,V> loHead = null, loTail = null; // 链表2存放在高位(原索引 + 旧数组长度) Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // 链表1 // 这个位运算的原理可以参考第三篇参考资料 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 链表2 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 链表1存放于原索引位置 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 链表2存放原索引加上旧数组长度的偏移量 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 复制代码
树化改造
链表长度太长,会被改造成红黑树。
当链表的长度超过MIN_TREEIFY_CAPACITY
最大树化临界值,就会进行树化改造。
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { ... } } 复制代码
为什么要树化?
**本质上是个安全问题。**因为链表查询影响性能,如果有人恶意造成哈希碰撞,就会构成哈希碰撞拒绝服务攻击,服务端CPU被大量占用用于链表查询,造成服务变慢或不可用。
源码系列文章
参考
JDK 源码中 HashMap 的 hash 方法原理是什么?胖君的回答
本文首发于我的个人博客 chaohang.top
作者张小超
转载请注明出处
欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。
这篇关于Java源码系列2——HashMap的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-01基于Python+Vue开发的医院门诊预约挂号系统
- 2024-10-01基于Python+Vue开发的旅游景区管理系统
- 2024-10-01RestfulAPI入门指南:打造简单易懂的API接口
- 2024-10-01初学者指南:了解和使用Server Action
- 2024-10-01Server Component入门指南:搭建与配置详解
- 2024-10-01React 中使用 useRequest 实现数据请求
- 2024-10-01使用 golang 将ETH账户的资产平均分散到其他账户
- 2024-10-01JWT用户校验课程:从入门到实践
- 2024-10-01Server Component课程入门指南
- 2024-09-30Dnd-Kit学习:新手快速入门指南