在jdk1.8中hashMap进行了较大优化,具体可以回答以下几点:
1. hashMap内部由 数组+链表转变为数组+链表+红黑树实现,当链表节点大于8时,存储结构由链表转为红黑树。
2. HashMap有两个参数影响其性能:初始容量和加载因子。默认初始容量是16(桶的数量),加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍,即当HashMap里面存了16*0.75=12个元素的时候,就会扩容,扩到32。
3. 如何定位key的数组索引值?
不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是“数组+链表+红黑树”的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表/红黑树,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。下面是定位哈希桶数组的源码:
// 代码1
static final int hash(Object key) { // 计算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代码2
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;
答案:1.首先计算key的hashCode值 ; 2.将hashCode的高16位参与运算,重新计算hash值;3.将hash值与table.length-1进行&(与)运算,得出最后的key对应的数组索引值
对于2的解释:在JDK1.8的实现中,优化了高位运算的算法,将hashCode的高16位与hashCode进行^(异或)运算,主要是为了在table的length较小的时候,让高位也参与运算,并且不会有太大的开销。
对于3的解释:为了尽可能散列开来使元素均匀分配,我们首先想到的就是把hash值对table长度取模运算,但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此JDK团队对取模运算进行了优化,使用上面代码2的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道HashMap底层数组的长度总是2的n次方,并且取模运算为“h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是HashMap在速度上的优化,因为&比%具有更高的效率。
4. 为什么HashMap的容量一定为2的n次幂?
为了使元素能够均匀分布,减少碰撞。
HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),hash%length==hash&(length-1)的前提是length是2的n次方;
为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;
例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;
其实就是按位“与”的时候,每一位都能 &1 ,也就是和1111……1111111进行与运算
5.扩容后,节点重hash为什么只可能分布在原索引位置与原索引+oldCap位置?
扩容代码中,使用节点的hash值跟oldCap进行位与运算,以此决定将节点分布到原索引位置或者原索引+oldCap位置上,这是为什么了?
假设老表的容量为16,即oldCap=16,则新表容量为16*2=32,假设节点1的hash值为0000 0000 0000 0000 0000 1111 0000 1010,节点2的hash值为0000 0000 0000 0000 0000 1111 0001 1010,则节点1和节点2在老表的索引位置计算如下图计算1,由于老表的长度限制,节点1和节点2的索引位置只取决于节点hash值的最后4位。再看计算2,计算2为新表的索引计算,可以知道如果两个节点在老表的索引位置相同,则新表的索引位置只取决于节点hash值倒数第5位的值,而此位置的值刚好为老表的容量值16,此时节点在新表的索引位置只有两种情况:原索引位置和原索引+oldCap位置(在此例中即为10和10+16=26)。由于结果只取决于节点hash值的倒数第5位,而此位置的值刚好为老表的容量值16,因此此时新表的索引位置的计算可以替换为计算3,直接使用节点的hash值与老表的容量16进行位于运算,如果结果为0则该节点在新表的索引位置为原索引位置,否则该节点在新表的索引位置为原索引+oldCap位置。
6.HashMap的存取机制
如何getValue?
get(key)方法得到key的hash值,然后计算hash&(table.length-1)得到桶的位置,然后判断桶的key是否与参数key相等,不等就遍历后面的元素找到相同key值,返回对应的value值即可.
如何put(key,value)?
1、hash(key)
,取key的hashcode进行高位运算,返回hash值
2、如果hash数组为空,直接resize()
3、对hash进行取模运算计算,得到key-value在数组中的存储位置i
(1)如果table[i] == null
,直接插入Node
(2)如果table[i] != null
,判断是否为红黑树p instanceof TreeNode
。
(3)如果是红黑树,则判断TreeNode是否已存在,如果存在则直接返回oldnode并更新;不存在则直接插入红黑树,++size
,超出threshold容量就扩容
(4)如果是链表,则判断Node是否已存在,如果存在则直接返回oldnode并更新;不存在则直接插入链表尾部,判断链表长度,如果大于8则转为红黑树存储,++size
,超出threshold容量就扩容
6. HashMap在jdk1.8之前的多线程扩容时的死循环问题
HashMap在1.7时,当进行扩容后,节点的顺序会反掉,所以,在多线程扩容后容易造成死循环(无限循环)问题。在JDK1.8后,扩容后,节点A和节点C的先后顺序跟扩容前是一样的。因此,即使此时有多个线程并发扩容,也不会出现死循环的情况。当然,这仍然改变不了HashMap仍是非并发安全,在并发下,还是要使用ConcurrentHashMap来代替。
7.HashMap和Hashtable的区别: