首先需要明确的是
HashMap 内部结构:可以看作是数组和链表结合组成的复合结构,数组被分为一个个桶(bucket),每个桶存储有一个或多个Entry对象,每个Entry对象包含三部分:key(键)、value(值),next(指向下一个Entry),通过哈希值决定了Entry对象在这个数组的寻址;哈希值相同的Entry对象(键值对),则以链表形式存储。如果链表大小超过树形转换的阈值(TREEIFY_THRESHOLD= 8),链表就会被改造为树形(红黑树)结构。
问:那HashMap在存储键值对时,是如何确定存储的位置的?
答:通过hashcode()方法计算键的哈希值==>得到的是一个整数。
int hash = key.hashcode()
JDK1.8中采用的是位运算。在特定情况下,位运算可以转换成取模运算(当 b = 2^n 时,a % b = a & (b - 1) )。也是因此,HashMap 才将初始长度设置为 16,且扩容只能是以 2 的倍数(2^n)扩容。源码中使用位运算hash & (length - 1) 代替取模运算hash%length,是利用其可以直接对内存数据进行操作,不需要转换成十进制,来提高运算效率。
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
问:若得到的数组下标相同怎么办?会如何插入?
答:如果该位置已有元素存储,会插入在链表的头部。
// 将新进元素指向链表的头部,再将头节点指向新元素
table[i] = new Entry(key,value,table[i]);
问:什么时候进行扩容?
答:当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
问:怎么进行扩容?(JDK1.8)
答: 图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:
问:为什么hashMap的容量扩容时一定是2的幂次方?
答:首先需要知道什么是与运算,都为1,才能得到1.我们在计算得出hash值后,是如何计算存储的位置的。利用的与运算,参考上面。
若HashMap容量为15,length-1的二进制位1110
那么两个索引的位置都是14,就会造成分布不均匀了,
增加了碰撞的几率,
减慢了查询的效率,
造成空间的浪费。
HashMap在1.7和1.8做的比较大的一个改变
-
-
JDK1.7是数组+链表,用的头插法——导致链表成环——如何成环的分析一下。
-
JDK1.8是数组+链表+红黑树,用尾插法来避免逆序。
-
-
-
JDK1.7直接用hash值和需要扩容的二进制数进行&与操作
-
JDK1.8沿用了JDK1.7的计算规律,也就是扩容前的位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7那种异或的操作,只需要判断hash值的新增参与运算的位是0还是1就可以了
-
扩容后JDK1.7
1.7之前使用的是数组加链表,它的数据节点是一个Entry节点,就是它的一个内部类,1.7之前数据插入的过程是使用头插法,但是HashMap使用头插法,它在扩容的一个过程,里面有一个resize方法,他又调用了一个transfer的方法,把里面的一些entry进行了一个rehash,在这个过程当中,可能会造成一个链表的环,就可能在下一次get的时候出现一个死循环的情况,其次它也没有加锁,在多线程并发的情况下,不能保证数据的安全性。
在多个线程并发扩容时,会在执行transfer()方法转移键值对时,造成链表成环,导致程序在执行get操作时形成死循环。
JDK1.8进行了改变,改为数组+链表+红黑树,把原来的一个Entry节点变成了一个Node节点,它整个put过程也做了一个优化。
参考
https://blog.csdn.net/u012156116/article/details/81206649
https://blog.csdn.net/u010890358/article/details/80496144
https://www.cnblogs.com/LiaHon/p/11149644.html