面试准备:基于JDK1.8的 hashMap源码

在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位置。




猜你喜欢

转载自blog.csdn.net/oYeYuanXinZhiZhu1/article/details/81017730
今日推荐