HashMap源码分析
一、HashCode为什么使用31作为碰撞值
-
如果选择大于31的数字会导致乘积运算时数据溢出,hashCode是用int作为返回值,通过碰撞计算可以得知,31的碰撞率最小,大于31的数字乘积会超出int的范围导致丢失数据信息。
二、HashMap的扰动函数
-
为什么使用扰动函数
-
使用扰动函数就是为了增加随机性
-
-
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
-
将hash值右移16位,也就是自己的一半,为啥是16,因为int类型长度为32,向右位移16位后做异或(^)计算,做异或计算的目的就是为了混合原hash值中的高位和低位,这样就增大了随机性,计算出的值再与长度做与(&)计算。
-
>>> 无符号位移,即全都是正整数
-
在初始化HashMap的时候需要一个2的倍数的长度,因为只有2的倍数-1的时候才可以出现0111这样的值,这也就证明了为什么map的默认长度要给16。如果给出其他的长度,例如给17,那么它的默认空间实际上是32,如果给出7,那么默认空间既8。
-
十进制快速计算二进制:
-
所有的2的幂次方都等于1+n个0的格式例如:2^1=10, 2^2=100, 2^3=1000, 2^4=10000, 2^5=100000..... 2^32=1*32个0
-
如果快速转十进制到二进制的话,可以如下计算 88 = 2^6 + 2^4 + 2^3 = 0101 1000
-
快速计算出临近的2的幂等值再相加即可
-
负数 -88 = 2^6 + 2^4 + 2^3 = 0101 1000 = 1010 1000,取反加1。
-
-
三、初始化容量和负载因子
-
散列数组需要一个2的倍数的长度,因为只有2的倍数-1的时候才会出现,0111这样的值。
-
// 初始化HashMap public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 如果大于int最大值则等于最大值,一般也不会有人这么设置 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 如果扰动值小于等于0或者不存在则报错 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; // 计算输入值临界2倍数的最小值 this.threshold = tableSizeFor(initialCapacity); } 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; }
-
负载因子-- 扩容使用
-
当数据量达到一定量级后就进行扩容,要选择一个合理的大小下进行扩容,默认值0.75就是说当阈值容量占了四分之三时就赶紧扩容,减少Hash碰撞保证性能,如果没有这个负载因子,可能就会一直绑定到某个节点的链表上,从而就失去了HashMap的性能。
-
四、扩容元素拆分
-
扩容最主要的问题就是拆分到新的数组中,当长度倍增扩容后,如果原Hash值和扩容新增出来的长度16进行&运算,如果值等于0则下标位置不变,如果不为0,那么新的位置则是原来位置上加16。涉及二进制的运算,有这个概念即可。
五、HashMap的插入、查找、删除
5.1 插入
-
插入HashMap时会先计算hash值以确定下标位置,当出现hash碰撞之后会出现以下的情况。
-
一个HashMap包含了以下结构,数组+链表和红黑树
-
当一个节点的hash碰撞过长时转化为红黑树
-
当链表长度大于8,且数组桶的长度大于64时,如果在64之内,则会先进行扩容操作。
-
-
转化为红黑树的时机:Hash碰撞导致链表长度大于8且数组桶的长度大于64。
-
链表树化的过程中是先由链表转换为树节点,此时的树可能不是一颗平衡树(既不会保证一个节点下有两个子节点),同时在树转换过程中会记录链表的顺序,主要方便后续树转链表和拆分更方便。
-
当链表转换成树后再进行红黑树的转换。
5.2 查找
-
先对传入的key值进行扰动函数的操作,获取新的hash值
-
下标的计算,确定了桶数组的下标位置之后就是对红黑树和链表进行查找和遍历操作了。
5.3 删除操作
-
查找,删除对应节点移动前后链表位置的数据。
-
这就体现出了和底层是数组的区别了,如果是数组操作,就会把未进行删除的元素重新赋值到一个新数组中,这时就出现瞬时的内存消耗增加。
5.4 遍历
-
链表情况下就是顺序遍历,红黑树遍历就是树根会移动到数组头部,由树根开始遍历。