传送门
一、HashMap结构示意图
个人总结注意事项:
1.JDK7时,HashMap采用数组+链表的结构, 链表插入为头插法, JDK8的时候, HashMap采用数组+链表+红黑树,链表插入元素为尾插法
2.HashMap是懒加载,调用完构造方法以后,并未创建对应的数组,只有在第一次存数据的时候调用resize()方法才会创建数组
3.构造方法有4个,
无参构造方法的容量和加载因子使用默认值
有参构造方法创建时使用阈值字段来暂时存储初始类容量大小,然后在第一次存数据的时候,调用resize()方法,使用阈值字段来创建数组,然后用阈值字段乘以加载因子重新赋值该字段
4.put()方法存数据时候,当链表长度大于默认阈值8的时候,会进入treeifyBin()方法,该方法里面会判断数组长度是否小于MIN_TREEIFY_CAPACITY=64,如果小于则进行扩容,否则则转换成红黑树
5.resize()扩容的时候,
源码前半部分判断主要分3个方面:(1) 非第一次扩容,最大值判断之后, 容量变为2倍 (2) 第一次扩容,调用有参构造方法,生成新容量值 (3) 第一次扩容,调用无参构造方法,生成新容量值
源码后半部分判断主要是将链表中的元素存入到新数组中
6.HashMap中单独定义了hash()方法,主要就是保证key的hash值更加散列
7.tableSizeFor()方法是返回大于输入参数且最近的2的整数次幂的数
8.当HashMap中的键值对的个数大于threshold值则进行扩容
9.虽然JDK8提供了红黑树的结构来保证数据,但是我们使用的时候通过 阈值8 和 最小树行化阈值64 还要扩容机制 可以看出来,我们平常使用到红黑树的情况还是很少,因为如果真的存储这么多元素,那么就可以使用其它的存储方式了,但是如果有的人真的较真,JDK官方还是提供了红黑树的结构
二、字段
1)静态常量字段
2)成员变量
三、构造方法
HashMap提供了4个构造方法
四、为什么HashMap底层树化标准的元素个数是8?
大概意思就是:如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了1-8长度的具体命中概率,当长度为8的时候,概率概率仅为0.00000006,这么小的概率,HashMap的红黑树转换几乎不会发生,因为我们日常使用不会存储那么多的数据,你会存上千万个数据到HashMap中吗?
五、hash算法
hash算法是计算其hash值,正好object类中提供了hashCode()方法,通过使用hashCode()方法,我们可以获得key对应的hash值。那我们直接拿这里的hash值与(长度-1)去做&运算不就得到对应的下标了吗?其实不然,因为table的长度都是2的幂,因此index仅与hash值的低n位有关,hash值的高位都被与操作置为0了,那么hash的高位的存在就等于毫无意义,这显然不是公平的算法。要是高16位也参与运算,会让得到的下标更加散列。
这段代码是什么意思呢?将key的hashcode与key右移16位进行异或得到的才是真正的hash值?
为了更好的理解,我们来看一个例子,假设table.length=16。
由上图可以发现,在计算下标时,只有hash值的低四位参与了下标的计算。key的hashcode与key右移16位进行异或得到的hash值,结合了前16位和后16位,所以相对来说是非常公平的。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
六、容量算法: tableSizeFor()方法
tableSizeFor的功能(不考虑大于最大容量的情况)是返回大于输入参数且最近的2的整数次幂的数。比如10,则返回16。该算法源码如下:
下面关于tableSizeFor的图形化分析参考另外博主的文章 tableSizeFor()源码图形化分析
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;
}
第一行很简单,为什么要-1放在最后说,最后一行是两个三目运算符,其中之一操作是n+1,都很容易理解。关键是中间五步移位加上或运算。
移位的思想
2的整数幂用二进制表示都是最高有效位为1,其余全是0,比如十进制8和32,下图只用了一个字节示意。
对任意十进制数转换为2的整数幂,结果是这个数本身的最高有效位的前一位变成1,最高有效位以及其后的位都变为0。
核心思想是,先将最高有效位以及其后的位都变为1,最后再+1,就进位到前一位变成1,其后所有的满2变0。所以关键是如何将最高有效位后面都变为1。
还是用图来示意。这里将十进制的25转换为32。
作者的做法是先移位,再或运算。
右移一位,再或运算,就有两位变为1;
右移两位,再或运算,就有四位变为1,,,
最后右移16位再或运算,保证32位的int类型整数最高有效位之后的位都能变为1.
初始值
选取任意int类型数字,下图x表示不确定0或者1.
我们目的是将所有的x变为1,如下图
最后+1,就能进位得到2的整数幂。
我们要做的就是不断通过右移+或运算来达到目的。
右移一位+或运算
可以看出,右移一位再或运算,有两位变成了1。
右移二位+或运算
右移两位再或运算,有四位变成了1。
右移四位+或运算
右移四位再或运算,有八位变成了1。
右移八位+或运算
右移八位再或运算,有十六位变成了1。
右移十六位+或运算
右移十六位再或运算,注意这里不是三十二位全变,而是最高位后面的全变1。
结果+1
可以看出,不管x是多少,我们都能将其转换为1。而且分别经过1,2,4,8,16次转换,不管这个int类型值多大,我们都会将其转换,只是值较小时,可能多做几次无意义操作。
初始容量-1
之所以在开始移位前先将容量-1,是为了避免给定容量已经是8,16这样2的幂时,不减一直接移位会导致得到的结果比预期大。比如预期16得到应该是16,直接移位的话会得到32。在上图中就是所有x本身已经是0的情况下,不减1得到的结果变大了。