什么是Map?
map就是用于存储键值对(<key,value>)的集合类,也可以说是一组键值对的映射(数学概念),它也是java中的一个顶级接口,下面有许多我们常用的map子类,如hashmap,concurrenthashmap等。
HashMap解析
数据结构(以1.8之后的HashMap结构为例子)
组成HashMap的结构为数组+线性链表+红黑树(1.8新增)。
我们以下面这段代理的运行为例子,讲一下HashMap结构。
/**
* @Author Dark traveler
* @Note 我心净处,何处不是西天。
* @Descrption
* @E-Mail : 1029149772@qq.com
* @Date : Created in 10:39 2020-3-27
*/
public class HashMapDemo {
public static void main(String[] args) {
Map<String,String> map = new HashMap<>(13);
map.put("author","Dark traveler");
System.out.println(map.get("author"));
}
}
(1)我们首先会去new一个数组。
为什么HashMap选择用数组?
*因为数组效率高,它可以通过索引下标很快找到我想取的数据,基本上一次就能定位到你数据所在的位置,时间复杂度为O(1)。.
(2)调用put方法插入键值对
它通过key这个对象的hashcode方法(正负都有可能)来计算出key对象的hashcode值,然后用这个值对第一步new出来的数组长度进行取模运算来得到键值对的索引。
*公式hashcode%hashmap.length<hashmap.length(我们猜想的公式,但实际是用位运算完成)
既然是取模运算,那么很有可能算出相同的索引值,这样怎么办呢?
所以,这里就引入了HashMap的第二种结构,链表,当通过取模运算得到相同的数组索引,那么它们在数组中的位置也相同,此时就会调用equcals方法,现比较它们的key的hashcode,如果不相同,就以链表的形式挂在下面。
那如果key的hashcode相同呢,我们都知道hash计算的空间利用率并不是那么高,所以当hashcode被算出来的时候,是有可能出现两个对象用同一个hashcode的情况,这也就是所谓的哈希碰撞,当索引相同时,随之比较链表中key的hashcode,当hashcode也相同的时候,就会把新的value值覆盖旧的value值。
(3)调用get方法获得key的value
*最完美的情况,并没有产生碰撞,那么所有的键值对都是平均分配在数组中,那么取值只需通过索引定位一次就能够取到,速度很快,时间复杂度为O(1)。
*产生碰撞的情况,取得的索引中形成了链表,我们都知道链表是这样一种结构,插入快,只需要改下元素得指针,但是查询却要通过遍历来查询,所以这样就导致了当查询到形成链表得那一块,时间复杂度为O(n),如果此时链表的长度为1千,1万,那么就大大的降低了效率。
*所以基于上面这种情况,在jdk1.8中,又加入了红黑树的结构,当链表节点>7的时候,链表结构会自动转换成红黑树,那么为什么是7呢?因为红黑树深度如果不够的话,反而会比较鸡肋,而这个关键节点数量就是7,超过7转换成红黑树就可以优化效率。
红黑树:一种接近平衡的二叉搜索树,在每个结点上增加一个存储位表示节点的颜色,可以是Red或Black,通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,它支持查找、插入、删除等操作,其时间复杂度最坏是O(logn)。
注:关于时间复杂度,我这里从别人博客上找了一张图,可以很好的解释概念。
源码解析
分析源码之前我们先从上面的数据结构上面提几个问题,看看我们能不能在源码中来找到答案。
*数组默认初始容量,我们一般new一个HashMap并不会去设置容量,那么它的初始容量是多少呢?
*它的取模运算真得是上面的公式吗?1.7和1.8是否有区别?
*HashMap扩容的负载因子loadFactor为什么是0.75?
*链表转红黑树的阈值真得是8吗?是不是如大部分人所说当链表长度为8的时候就转红黑树呢?
*变了红黑树以后,如果我移除了很多节点,它是不是又会变回链表呢?
*在开发过程中用哪一种初始化HashMap方法最好呢?
好,那么我们先进入HashMap类去看看,通过代码来验证上面那些问题。
运算方法,带左移4位 ,左移四位(左移不分有无符号,都是在后面补0)
00000000 00000000 0000 0001 << 4
00000000 00000000 0001 0000 = 16
1、当我们拉过一大段的注释之后,我们可以看到, HashMap有个final的成员变量,默认初始化容量为 1<<4,后面有个注释,16,没错,HashMap的默认容量是16,并且上面还有个注释,默认初始容量-必须是2的幂。
什么?我刚刚明明传了个13,也没问题不是,我们来看看它在初始化里面做了什么?连续根了三个方法,我们发现了方法tableSizeFor(int cap),我们发现虽然我们传了13进去,但是它通过移位运算把这个13变成了16,也就是变成了比cap大的最小的2的幂次方的值,比13大且是2的指数次幂的值就是16,我们从它的注释中也能看出,Returns a power of two size for the given target capacity,根据你传的初始化容量,返回一个2的指数次幂,所以答案揭晓了,它把我们传的13变成了16,符合2的指数次幂且比13大。
举个例子,如果传进来的cap是13。
n = 13 -1 =12
12的二进制: 00000000 00000000 00000000 0000 1100
我们先来分析下
n |= n >>> 1; 先把n无符号右移1位,再进行按位或运算
n>>>1 无符号右移1位,也就是说向右移动1位,最高位补0,不管该数是正是负,那么负数会变成正数
按位或运算概念:相同二进制位上面,都是0则为0,否则为1。
n=12 00000000 00000000 00000000 0000 1100
n>>>1 0 00000000 00000000 00000000 0000 110 = 6
-----------------------------------------------------------------------------------------------------
n=12 00000000 00000000 00000000 0000 1100
n>>>1=6 00000000 00000000 00000000 0000 0110
n = n | n>>>1 00000000 00000000 00000000 0000 1110 = 14
-------------------------------------------------------------------------------------------------------
n=14 00000000 00000000 00000000 0000 1110
n>>>2 00000000 00000000 00000000 0000 0011
n = n| n>>>2 00000000 00000000 00000000 0000 1111 = 15
-------------------------------------------------------------------------------------------------------
n=15 00000000 00000000 00000000 0000 1111
n>>>4 00000000 00000000 00000000 0000 0000
n = n| n>>>4 00000000 00000000 00000000 0000 1111 = 15
-------------------------------------------------------------------------------------------------------
n=15 00000000 00000000 00000000 0000 1111
n>>>8 00000000 00000000 00000000 0000 0000
n = n| n>>>8 00000000 00000000 00000000 0000 1111 = 15
-------------------------------------------------------------------------------------------------------
n=15 00000000 00000000 00000000 0000 1111
n>>>16 00000000 00000000 00000000 0000 0000
n = n| n>>>16 00000000 00000000 00000000 0000 1111 = 15
你会发现经过这几次移位运算以后,n的值停留在了15,当后4位都是1以后后面的运算就没有意义了。
所以你会发现,它的目的就是要把你传进来的数,取到你这个数二进制为1的最高位,然后把后面全变成1,这也就是为什么它第一步要把你传进来的值减1,如果你传的刚好是2的幂次方,如果不减1它就会向上进一位了,也就是16的话会变成31,显然这不是它想看到的。
最后通过两个三目判断下,第一个三目判断如果你传的是0,就改成1,那二个三目判断如果你的数不是大于1<<30,就让你的数加1,上面的15+1 也就变成了16。
说完了改变数组长度的问题,再看看它把你的这个改变后的长度放到了哪里。
拿到你的值返回到上面,你会发现,它并没有把这个16变成数组的长度,而是把它给了threshold这个变量,并不是我们想象中的new一个长度为16的数组,这是为什么呢?
很不解,我们继续看它的其它构造方法,一般我们都用无参,我们看下无参构造,我们发现它也只是赋值了一个负载因子为0.75而已,那么它到底在哪里new了我们的数组呢。
2、初始化方法看完,我们就看它的put方法。
首先看它的第一个put方法,它把生成的hashcode又进行了一次移位运算,为什么?
我们现在来分析下这个按位异或运算。
按位异或运算概念:二进制位上数字相同就为0,否则为1。
我们这里举一个例子,说明下有这个按位异或的扰动运算和没有的差别。
如果没有这个扰动运算的情况。(数组索引的公式为 hashcode&(数组长度-1),下面有详细讲解)
加上扰动运算后的情况。
很显然我们会发现,在产生一些差hashcode的情况下,减少了hash碰撞的概率。
好,看完它对hashcode的优化以后,我们回到上面的问题,什么时候new数组?看代码我们发现当这个HashMap中的数组table==null或者数组的长度==0的时候,它会调用一个resize()方法。
那么我们再来看看这个resize()方法,我们把threshold带进去看看,因为初始化,所以并没有这个数组,我们只需要关注数组初始化过程。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //table为null,所以oldCap为0
int oldThr = threshold; //threshold = 16 = oldThr
int newCap, newThr = 0;
if (oldCap > 0) { //第一次进来不大于0
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
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; //大于0,所以 newcap(新数组长度) = 12 = threshold
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor; //ft = 16*0.75
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); //newThr = 12
}
threshold = newThr; // threshold = 8
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //重点,newTab【16】
table = newTab; //table = newTab【16】
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 { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
所以我们发现。它是通过putVal这个方法中的resize()方法来new初始数组的,并且第一次确实把threshold赋值给了newCap,也就是确实构造了一个长度为16的新数组。
我们再来看看我们最关心的数组索引的运算。
我们想的取模运算:
index = key.hasCode() % n
HashMap的取模运算:
n = (tab = resize()).length;
(p = tab[i = (n - 1) & hash])
//看它索引计算方式,哈希值与数组的长度-1来按位与
i = (n - 1) & hash
按位与运算的概念:相同二进制位上面,都是1结果为1,否则为0。
&运算这里举个例子,假设数组长度为8,有两个hash值分别为3和10。
常规公式 hashcode%length 算出的索引为 3 和 2
移位公式 hashcode&(length-1)
当然,其实它们的二进制是32位,我们这里就取后8位,因为前面都是0不会影响。
hashcode = 3 = 0000 0011
length-1 = 7 = 0000 0111
-----------------------------------------------
0000 0011 = 3
hashcode = 10 = 0000 1010
length-1 = 7 = 0000 0111
----------------------------------------------
0000 0010 = 2
发现没有,它做出的来答案和取模运算是一样的,但二进制层的运算效率比直接的取模要高很多。
好,那么问题来了,作者为什么要把数组的长度设置成2的指数次幂。
我们现在假设数组的长度是7
hashcode = 3 = 0000 0011
length-1 = 6 = 0000 0110
-----------------------------------------------
0000 0010 = 2
hashcode = 10 = 0000 1010
length-1 = 6 = 0000 0110
----------------------------------------------
0000 0010 = 2
这样就很容易变成算出的索引值相同了,当然我举的这个例子也不符合取模公式了,所以为什么要容量为2的指数次幂,就是增加运算效率和减少出现同位索引的情况。
接着往下看putVal方法。首先,我们存一个键值对进来,分析好索引以后,如果这个数组的索引里面没有值,它会直接new一个node放在这个索引位置中。
如果这个索引位置不为空呢?
首先看第一个条件,索引槽中key的hashcode和你传进来的key的hashcode相等,且这两个key对象的地址相同或者它们equals相同,说白了,就是它们是同一个对象的情况下,会把老的节点替换成新的节点,这里也就说明了为什么当你传相同的字符串形式的key的时候,value会覆盖替换了。
在看第二个条件,如果不符合上面那种情况呢,他会先判断这个原索引槽的节点是不是一个树节点,如果是,在这个树节点后面挂上一个树节点。
如果上面两种情况都不满足呢,那不就是还没变成树,传进来的key对象又不是同一个,那么它就会把节点直接以链表的形式挂到最后一个元素后面。
说白了里面就两个关键点,一个死循环里面,放两个条件,第一条件,如果当前是这个链表的next元素为null,那就把这个元素挂在上面,如果不为null,那就是第二个条件,那么就可以知道这个链表肯定不止一个元素了,那么就找到这个第二个元素,和上面一样比较一下它们的key的hash看下是不是同一个,如果是同一个就直接跳出这个死循环,如果不是同一个就继续比较,一直比较到这个链表的最后一个为止,并且把这个新节点节点挂在最后,跳出,同时第一个条件里面还有一个判断,如果binCount = 7,也就是这个循环做了8次的时候,调用转红黑树方法,然后跳出死循环。
但是上一步留了一个坑,如果在第二个if跳出,那么e就会等于当前那个和插进来的值key相同的值,它只知道这个以后直接跳出到下一个方法。
如果这个e!=null,也就是那个新节点没有挂载在后面的情况下,它又做了个替换,把当前相同的key的新value,替换老value,同时它会把这个oldValue返回出来,告诉你,这里替换值了。
所以这里有个小tips,当你传的key是同一个的时候,你会发现put方法有返回值哦?它的返回值是你替换的oldValue。
好了,这就是整个putVal方法了,也就是HashMap里面个人认为最经典的方法了。
——————————————————————————————————————————————————--————
现在我们再回到这个resize()方法,我们来看看HashMap是怎么扩容的。
首先我们来了解这样一个概念,也是运算的,假设我第一次的数组长度是16,有两个hashCode如下,key1和key,我们通过计算索引的公式hashcode&(map.length-1),得到在数组长度为16的时候,这两个hashcode所得到的索引都是5,那么它们毫无疑问在数组索引5的地方形成了链表。
此时我们数组发生扩容,扩容一次达到了32,它扩容就是<<1,左移一位也就是*2,那么我们此时再调用这个公式hashcode&(map.length-1),得到在数组长度为32的时候,这两个hashcode所得的索引一个是5,一个是21,那么就得出一个结论,新数组的索引要不就是原索引,要不就是原索引+原数组长度。
好那么哪一种是原索引,哪一种又是原索引+原数组长度呢。
如果hashcode&原数组长度,得出的结果是0,那么就算是低位,就还是放在原来的索引中,如果hashcode&原数组长度得出的结果不为0,就放到原索引+原数组长度的索引中。
好,我们了解了这个概念以后我们再来看源码。
第一段我们可以看出,它确实是以左移1位来扩容的,也就是容量*2,然后我们看关键的新算法。
看到了吧,用这个节点的hashcode和oldCap进行按位&运算,分别得出两个链表,一个是结果为0的,一个是结果不为0的,然后 把结果为0的链表放到原来的索引newTab【j】中,不为0的链表放到newTab【j+oldCap】中,而这个j就是遍历老的数组的当前索引,这样就再也不用rehash,然后重新再进行一波索引的计算了,只需要看它们的高位就行了,大大提高了效率。
————————————————————————————————————————————————————————
好,那除了速度更快它还有啥好处呢,为什么1.8以后要把rehash方法去了,换成这个呢?因为之前1.7中还有一个很致命的原因,在高并发环境下,rehash可能导致死锁,而导致这个致命原因的代码就出在这里(do-while循环中),如下:
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了旧的Entry数组
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
Entry<K, V> e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
do {
Entry<K, V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i]; //标记[1]
newTable[i] = e; //将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
为什么说它可能导致死锁呢,举个例子,假设线程一和线程二同时对共享的变量一个HashMap进行扩容,并且同时产生了两个新的长度的数组,此时进行进行计算把新数组的索引指向老数组的索引,该索引上的元素会被线程一的扩容数组和线程二的扩容数组指向,但老数组key里面对象的value只有一份,这里假设这个key中只有两个链表元素,那么总有一个会先把链表元素搬过去,先搬过去的那个扩容数组会把这个链表元素前后节点互换,就是上面那些代码的操作,但是此时后一个指向这个链表元素节点的扩容数组指针却没有改变,它会把前一个扩容数组的节点原封不动的搬下来,这样就造成了这个链表元素头指向尾部,尾部指向头部,形成一个闭环,也就是死锁,所以在1.8中使用了这种新的扩容办法来避免这个问题。
但是虽然避免了死锁它依然是不安全的,虽然它不再是倒插式放到新数组中,但依然会存在覆盖问题,举个例子,当线程一已经移动完成,同时对应的链表又插入新值,而线程二中才刚刚扩容完毕并没有新链表节点的指针,那就依然会把刚刚插入链表的新值给覆盖掉。
3、负载因子是0.75的原因,我们先看看原作者的解释,说白了就是一个时间和空间成本的权衡,当你一个数组长为16的时候,它不会当你站满所有槽再来扩容。我们都知道链表长度越长,get的时候复杂度越高(时间换空间),反之,链表越短那么效率确实会提高,查询时间就会短很多(空间换时间),那么通过实验表明,当load=0.5的时候时间开销最佳,当load=1的时候,空间开销最佳。
所以,根据牛顿二项式推导出来log2约等于0.698,作者干脆就取了个折中值0.75。
4、阈值为8的原因,同样我们看看原作者的解释。这里就涉及到一个概率统计的公式泊松分布,作者根据这个公式得出现链表长度为8的时候概率是0.00000006,也就是亿分之六,已经无限趋向于0了,虽然1.8加上了红黑树,但是小数据量很难产生,除非数据量非常庞大,达到十万,百万的时候才可能会慢慢产生,当然前提是加载因子必须是0.75。
但是,事实真得就是这样吗?我们再看到putVal方法的转红黑树的地方,如下图,我们看看这个循环,我们发现它的意思是,当产生了相同的哈希索引的时候,给初始节点的尾部挂上下一个节点,首先要有一个节点站住了这个数组的索引位,才会开始挂载这个相同哈希索引的节点。
那么我们知道了它的想法以后,我们用for循环写下它的结构,看看它结构到底是怎么变化的。
/** binCount值 链表长度
* 0 2
* 1 3
* 2 4
* 3 5
* 4 6
* 5 7
* 6 8
* 7 9
*/
int length = 1;
for(int binCount =0;;++binCount){
System.out.println(binCount);
{
//当数组这个位置已经占有一个节点以后,开始挂载链表
length = 1 + length;
}
if(binCount>= 8-1){
System.out.println("此时链表长度"+length);
System.out.println("循环运行次数"+(binCount+1));
System.out.println("调用红黑树方法");
break;
}
}
因为节点赋值的方法在我们判断方法的上面,所以你会发现,当这个地方触发红黑树转变的方法的时候,此时链表的长度应该是9,也就是说数组中有一个节点,它下面挂着8个节点,再回去看看那个阈值,作者的解释分明是从0开始的,看到这里小伙伴们应该明白了吧,阈值为8的时候,链表的长度其实是9啊。当然,看到这里是不是有种恍然大悟的感觉,但事情的真相不止如此,我们再进入转红黑树的方法看看。
天呐!它里面还有一个判断,当数组长度不超过64的时候,我们只是调用了扩容方法,并没有转红黑树,只有超过64的时候,才会调用下面转红黑树的方法。
所以,事情的真相只有一个,当链表的长度为9(包含了数组中的那一个),且数组的长度大于64的时候,此时才会调用转红黑树的方法,而好多地方说什么大于8就转红黑树,千万不能理解为链表的长度为8(本人就听很多人说过这个错误的解释),大于8是作者给出的阈值,而这个阈值是从0开始的,也就是类似于索引0开始一直到8,所以链表的长度为9的节点在1.8以后的HashMap中是看不到的了。
好,假如我们现在都把这些条件满足了,我们来看看这个红黑树是怎么转换的。
你会发现它还说遍历那个链表,把第一个取节点取出来,把它变成一个TreeNode,并且把它变成hd,只会其它的节点都以指针的方式以此挂载,那不还是链表嘛,只是把node变成了treeNode,没错,但是最后它还有个方法,如果这个树的头节点不为null,也就是我们转换链表的第一个节点,既然转换了当然不为null,那就调用treeify方法,把这整个tab传进去,把这段链表改成红黑树。
5、它确实会变回链表,当下面的链表节点<=6的时候,它就会转换回链表,从作者设置的这个值可以看出,这里是为什么我也没有去深究,但想一想肯定无法也是时间和空间效率的平衡,这里我在网上找了一个我所能接受的解释。
6、在阿里巴巴开发手册中有一段这样的描述,HashMap初始化时建议指定它的初始值大小,也就是建议使用带初始值大小的那个初始化方式,为什么呢?
原因:如果不先指定大小,HashMap可能会经历多次扩容,比如你要存几百个key-value,不预先指定大小的话,它会从16一路扩容上来,会触发多次resize方法,引发不必要的消耗。
那么建议怎么选择初始容量大小呢,假如我要存7个值,那么就把初始容量设置为7嘛?
显然是不对的,经过大量的实验,阿里巴巴给出了这样一个公式。
把7带入,initialCapacity = 7/0.75 + 1 = 10,也就是说你初始值传10是最好的。
如果你传的是7的话,他就便成8了,而8*0.75 = 6,它很可能在存6个的时候就扩容了,所以传10变成16,基本上就不会去扩容。
总结:
1、HashMap的默认初始容量是16,而且构造方法中会把你自定义的长度转换成向上的最近的2的n次方幂,它是通过移位算法来帮你转的,移位算法中为什么要把你传的cap-1,是因为防止你传的刚好是2的n次方幂导致它多往高位进一位。
2、HashMap的计算hash索引索引方式是hashcode&(table.length - 1),是一种更快的取模运算,也是为什么要把数组的长度定为2的n次方幂的原因,这样可以通过这个运算增加运算效率,在jdk1.8中还加入了一个扰位运算,把hashcode无符号向右移动16位再与原hashcode进行按位异或运算,减少hash碰撞概率。
3、HashMap的扩容方法,从1.7的rehash(从新计算一遍hash索引),改成了现在的resize()方法,主要是去除了transfer()方法,新的高位索引通过原位置+原数组长度来得到,同时解决了产生死锁的问题,而且1.8中的初始化table长度方法也放到了put中,不再是初始化的时候就new一个数组了,而是调用put的时候再去生成你要的数组。
4、负载因子7.5的由来是通过牛顿二项式计算而来的。
5、HashMap在1.8中,转红黑树的阈值是8,但是实际的链表长度为9(如果不算上数组中的那个节点才是8)且数组长度大于64的时候才会转成红黑树,而转成红黑树以后,当红黑树的链表节点少于或等于6位的时候,红黑树又会转化回链表。
6、工作中,初始化HashMap最好的方式就是,传入 (需要存储的个数/负载因子+1)的公式算出来的初始容量大小来对HashMap进行初始化,最大程度的避免扩容。