带你走进Java集合_HashMap源码分析_彻底理解HashMap的底层数据结构

上一篇文章主要从源码角度讲解了为什么HashMap底层容器的大小必须是2的整数次幂,接下来几篇博文将着重介绍HashMap的底层数据结构,同时这也是面试的重点。我们知道HashMap的底层数据结构:数组+链表+红黑树

《带你走进Java集合_HashMap源码分析_彻底理解HashMap的底层数据结构》

在hashMap的源码中有一个非常重要的属性Node[]tab,这个属性就是HashMap底层数据结构中的数组,我们put的每一个值首先会封装成Node放到数组中。我们通过调用HashMap的put的方法去分析HashMap的数据结构。

下面我们以示例为导向进行源码的讲解:

Map<String,String>map = new HashMap<String, String>();----------------(code-1)
        map.put("a","a");---------------------------------------------(code-2)
        map.put("b","b");---------------------------------------------(code-3)
        map.put("aa","aa");-------------------------------------------(code-4)
        map.put("ab","ab");-------------------------------------------(code-5)

通过计算:key=”a”的下标i=1,key=”b”的下标i=2,key=”aa”的下标i=0,key=”ab”的下标i=1

第一个讲解点:数组tab的初始化。

我们知道当调用HashMap的构造函数时(code-1),底层数组tab并没有初始化,初始化的工作放到了第一次调用put的方法(code-2)。源码如下:

 Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

上面这段代码就是tab初始化的,真正的动作放到了resize()中,resize()这个方法我们接下来的文章会重点讲解,现在我们就知道底层数组的初始化是在第一次调用put时进行的。初始化后tab.length=16(因为我们用无参构造),此时tab是一个长度16的空数组,还没有元素

《带你走进Java集合_HashMap源码分析_彻底理解HashMap的底层数据结构》

第二个讲解点:向HashMap中添加数据,首先要找到tab中相对应的下标

在上一篇文章中我们知道了获取插入到tab数组的下标值:i = (n – 1) & hash,所以我们新增的数据存放的位置就是tab[i]。

p = tab[i = (n - 1) & hash]

上面的示例当key=”a”,通过i=(n-1)&hash计算得知i=1。

《带你走进Java集合_HashMap源码分析_彻底理解HashMap的底层数据结构》

第三个讲解点:如果key的hash没有冲突,我们新增的值直接会封装成Node,放到向对应的下标中。

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

当key=”a”,tab[i]=tab[1],而此时下标1还没有元素,所以会执行newNode方法,就是把用户给的key,value封装成Node,然后放入数组tab[1]中。此时就变成了如图:

《带你走进Java集合_HashMap源码分析_彻底理解HashMap的底层数据结构》

第四个讲解点:如果key的hash冲突,这时数据结构链表就登场了。

当我们新增key=”ab”时,通过计算下标i=1,所以要放到tab[1]下面,我们上面分析了,tab[1]已经放了key=”a”,这样就冲突了,为了解决这种冲突,HashMap采用了链表。

《带你走进Java集合_HashMap源码分析_彻底理解HashMap的底层数据结构》

我们先不考虑扩容,只要往HashMap中添加数据都会进行上面的方式,首先获取添加数据key对应的下标i,判断此下标是否已经存放数据了,如果没有存放数据,则直接把新增的数据放到数组中,如果此下标已经存放了数据,则把新增的数据放到链表的末尾。

第五个讲解点:当上面的链表长度>8时,这个链表就会变成红黑树。

如果hash在tab[1]上冲突,一直往链表末尾添加,但是链表的时间复杂度o(n),当链表过长时,性能上会下降,基于性能的考虑,从JDK8以后,链表不能无限期增大,当到一定长度后就会变成红黑树,而红黑树的时间复杂度o(logn),链表变成红黑树我们在上面的文章中介绍过有一个阀门TREEIFY_THRESHOLD=8,当超过这个阀门就会变成红黑树。

至于红黑树的数据结构,我会专门写一篇文章,请持续关注。

下面我们总结一下:

1)HashMap的底层数据结构数组+链表+红黑树

2)数组tab初始化工作在第一次调用put时进行的。

3)当key计算的下标i=(n-1)&hash,没有冲突时,会直接把数据放到数组中tab[i].

4)如果key计算的下标i=(n-1)&hash有冲突,则会把数据放到链表的末尾。

5)如果链表的长度大于阀门TREEIFY_THRESHOLD(8)时,基于性能的考虑,链表会变成红黑树

上面5点总结了HashMap数据结构的流程,但是HashMap的hash设计的非常的好,hash冲突的概率低,虽然HashMap设计了这样复杂的数组+链表+红黑树的数据结构,但是由于上面数组大小必须是2的整数次幂,hash是通过key的hashCode的高16位和低16位,在通过(n-1)&hash计算出来的非常的均匀,所以用到HashMap时,很少会真正用到红黑树。

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/shaotianqiang/article/details/80816394
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞