摘要:剖析Map接口的具体应用以及HashMap的底层是如何实现的?

本文分享自华为云社区《【图文并茂】深度解析HashMap高频面试及底层实现构造!【奔跑吧!JAVA】》,原文作者:灰小猿 。

Map接口大家应该都据说过吧?它是在Java中对键值对进行存储的一种罕用形式,同样其中的HashMap我置信大家应该也不会生疏,一说到HashMap,我想略微晓得点的小伙伴应该都说是:这是存储键值对的,存储形式是数组加链表的模式。然而其中真正是如何进行存储以及它的底层架构是如何实现的,这些你有理解吗?

可能很多小伙伴该说了,我只须要晓得它怎么应用就能够了,不须要晓得它的底层实现,但其实恰恰相反,只晓得它怎么应用是齐全不够的,而且在Java开发的面试之中,HashMap底层实现的发问和考查曾经是司空见惯的了。所以明天我就来和大家剖析一下Map接口的具体应用以及HashMap的底层是如何实现的?

小伙伴们缓缓往下看,看完相对会让你播种满满的!

1,Map接口和List接口是什么关系?

对于这个问题,如果非要说这两个接口之间存在怎么的关系的话,那无非就只有一个,就都是汇合。存放数据的。在其余下面,Map接口和List接口的分割其实并不大,为什么这么说?

先来看List接口,对于List接口我在之前也和大家提到过,它是继承于Collection接口的,是Collection接口的子接口,只是用于对数据的单列存储。继承关系如下图:

而Map接口是一个顶层接口,上面蕴含了很多不同的实现类,它是用于对键值对(key:value)进行存储的,继承关系如下图:

所以Map接口和List接口的关系和应用不要混同了!

2、Map有哪些罕用的实现类?

下面对于Map的继承构造咱们曾经理解了,咱们也看了其中很多不同的实现类,这些类很多也是咱们比拟相熟的,比方HashMap、TreeMap以及HashTable。在面试的时候,面试官往往就还会问,Map接口下有哪些罕用的实现类以及它们的作用,那么接下来咱们就来对这几个接口进行简略的介绍和剖析一下,

HashMap:下面也说了,HashMap的底层实现是数组+链表+红黑树的模式的,同时它的数组的默认初始容量是16、扩容因子为0.75,每次采纳2倍的扩容。也就是说,每当咱们数组中的存储容量达到75%的时候,就须要对数组容量进行2倍的扩容。

HashTable:HashTable接口是线程平安,然而很早之前有应用,当初简直属于一个遗留类了,在开发中不倡议应用。

ConcurrentHashMap:这是现阶段应用应用比拟多的一种线程平安的Map实现类。在1.7以前应用的是分段锁机制实现的线程平安的。然而在1.8当前应用synchronized关键字实现的线程平安。

其中对于HashMap的考查和发问在面试中是最频繁的,这也是在日常开发中最应该深刻了解和把握的。所以接下来就次要和大家详细分析一下HashMap的实现原理以及面试中的常考问题。

3、请论述HashMap的put过程?

咱们晓得HaahMap应用put的形式进行数据的存储,其中有两个参数,别离是key和value,那么对于这个键值对是如何进行贮存的呢?咱们接下来进行剖析一下。

在HashMap中应用的是数组+链表的实现形式,在HashMap的下层应用数组的模式对“雷同”的key进行存储,上层对相应的key和value应用链表的模式进行链接和存储。

留神:这里所说的雷同并不一定是key的数值雷同,而是存在某种雷同的特色,具体是哪种特色骂咱们持续往下看!

HashMap将将要存储的值依照key计算其对应的数组下标,如果对应的数组下标的地位上是没有元素的,那么就将存储的元素寄存下来,然而如果该地位上曾经存在元素了,那么这就须要用到咱们下面所说的链表存储了,将数据依照链表的存储程序顺次向下存储就能够了。这就是put的简略过程,存储后果如下:

然而咱们有时候存储的数据会很多,那么如果始终应用链表的模式进行数据的存储的话就或造成咱们的链表的长度十分大,这样无论在进行删除还是在进行插入操作都是非常麻烦的,因而对于这种状况应该怎么办呢?

这里就波及到了一个链表中数据存储时,进行“树化”和“链化”的一个过程,那么什么是“树化”和“链化”呢?

当咱们在对键值对进行存储的时候,如果咱们在同一个数组下标下存储的数据过多的话,就会造成咱们的链表长度过长,导致进行删除和插入操作比拟麻烦,所以在java中规定,当链表长度大于8时,咱们会对链表进行“树化”操作,将其转换成一颗红黑树(一种二叉树,右边节点的值小于根节点,左边节点的值大于根节点),这样咱们在对元素进行查找时,就相似于进行二分查找了,这样的查找效率就会大大增加。

然而当咱们进行删除操作,将其中的某些节点删除了之后,链表的长度不再大于8了,这个时候怎么办?难道就要连忙将红黑树转化为链表的模式吗?其实并不是,只有当链表的长度小于6的时候,咱们才会将红黑树从新转化为链表,这个过程就叫做“链化”。

过程图示如下:

那么为什么要在长度8的时候进行“树化”,而在长度小于6的时候才进行“链化”呢?为什么不间接在长度小于8的时候就进行“链化”?

次要起因是因为:当删除一个元素,链表长度小于8的时候间接进行“链化”,而再减少一个元素,长度又等于8的时候,又要进行“树化”,这样重复的进行“链化”和“树化”操作特地的耗费工夫,而且也比拟麻烦。所以程序就规定,只有当当链表长度大于等于8的时候才进行“树化”,而长度小于6的时候才进行“链化”,其中对于8树化、6链化这两个阈值心愿大家牢记!

4、链表中是依照怎么的程序存放数据的?

咱们当初曾经晓得了HashMap中的元素是如何寄存的,然而有时候面试官可能还会问咱们,在HashMap中,向链表中存储元素是在头结点存储的还是在尾节点存储的?

这个咱们须要晓得,对于HashMap中链表元素的存储。

在JDK1.7以及前是在头结点插入的,在JDK1.8之后是在尾节点插入的。

5、Hash(key)办法是如何实现的?

咱们当初曾经晓得了HashMap中的元素是如何存储的了,那么当初就是如何应该依据key值进行相应的数组下标的计算呢?

咱们晓得HashMap的初始容量是16位,那么对于初始的16个数据位,如果将数据依照key的值进行计算存储,个别最简略的办法就是依据key值获取到一个int值,办法是:

int hashCode = key.hashCode()而后对获取到的hashCode与16进行取余运算,hashCode % 16 = 0~15

这样失去的永远都是0—15的下标。这也是最最原始的计算hash(key)的办法。

然而在理论状况下,这种办法计算的hash(key)并不是最优,寄存到数组中的元素并不是最扩散的,而且在计算机中进行余运算其实是十分不不便的、

所以为了计算结果尽可能的离散,当初计算数组下标最罕用的办法是:先依据key的值计算到一个hashCode,将hashCode的高18位二进制和低18位二进制进行异或运算,失去的后果再与以后数组长度减一进行与运算。最终失去一个数组下标,过程如下:

int hashCode = key.hashCode()int hash = hash(key) = key.hashCode()的高16位^低16位&(n-1)  其中n是以后数组长度

同时在这里要揭示一点:

在JDK1.7和JDK1.8的时候对hash(key)的计算是略有不同的

JDK1.8时,计算hash(key)进行了两次扰动

JDK1.7时,计算hash(key)进行了九次扰动,别离是四次位运算和五次异或运算

其中扰动可能了解为运算次数

以上就是Hash(key)办法的实现过程。

6、为什么HashMap的容量始终是2的倍数?

HashMap的容量之所以始终是2的倍数,其实是与下面所说的hash(key)算法无关的。

起因是只有参加hash(key)的算法的(n-1)的值尽可能都是1的时候,失去的值才是离散的。如果咱们以后的数组长度是16,二进制示意是10000,n-1之后是01111,使得n-1的值尽可能都是1,对于其余是2的倍数的值减1之后失去值也是这样的。

所以只有当数组的容量长度是2的倍数的时候,计算失去的hash(key)的值才有可能是绝对离散的,

7、Hash抵触如何解决?

什么是Hash抵触?就是当我计算到某一个数组下标的时候,该下标上曾经寄存元素了,这就叫Hash抵触,很显然,如果咱们计算数组下标的算法不够优良的时候,很容易将存储的数据积攒到同一个下标下面,造成过多的Hash抵触。

那么如何解决hash抵触?

最应该解决的其实就是让存储的key计算失去的数组下标尽可能的离散,也就是要求hash(key)尽可能的优化,数组长度是2的倍数。这也就是Hash抵触的次要解决办法。

具体能够查看上面HashMap要害局部的底层源码:

Hash(key)的底层实现

 /**     * Applies a supplemental hash function to a given hashCode, which     * defends against poor quality hash functions.  This is critical     * because HashMap uses power-of-two length hash tables, that     * otherwise encounter collisions for hashCodes that do not differ     * in lower bits. Note: Null keys always map to hash 0, thus index 0.     */    static int hash(int h) {        // This function ensures that hashCodes that differ only by        // constant multiples at each bit position have a bounded        // number of collisions (approximately 8 at default load factor).        h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }

put(key,value)办法的底层实现

 /**     * Associates the specified value with the specified key in this map.     * If the map previously contained a mapping for the key, the old     * value is replaced.     *     * @param key key with which the specified value is to be associated     * @param value value to be associated with the specified key     * @return the previous value associated with <tt>key</tt>, or     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.     *         (A <tt>null</tt> return can also indicate that the map     *         previously associated <tt>null</tt> with <tt>key</tt>.)     */    public V put(K key, V value) {        if (key == null)            return putForNullKey(value);        int hash = hash(key.hashCode());        int i = indexFor(hash, table.length);        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(hash, key, value, i);        return null;    }

8、HashMap是如何扩容的?

咱们在下面说到了HashMap的数组的初始容量是16,然而很显然16个存储位是显然不够的,那么HashMap应该如何扩容呢?

在这里须要用到一个参数叫“扩容因子”,在HashMap中“扩容因子”的大小是0.75,

咱们下面也提到过,对于初始长度为16的数组,当其中存储的数据长度等于16*0.75=12时。就会对数组元素进行扩容,扩容量是原来数组容量的2倍,也就是以后是15话,再扩容就是扩容32个数据位。

9、扩容后元素怎么寄存的?

咱们晓得HashMap的数组在进行扩容之后,数组长度是减少的,那么这个时候,前面新扩容的局部就是空的。然而这个时候咱们就应该让前面的数据位空着吗?显然是不可能的,这样会造成内存的很大节约。

因而在HashMap的数组扩容之后,原先HashMap数组中寄存的数据元素会进行从新的地位调配,从新将元素在新数组中进行存储。以充分利用数组空间。

10、JDK1.7和JDK1.8对HashMap的实现比拟

在JDK1.7和JDK1.8中对HashMap的实现是略有不同的,最初咱们依据下面的解说对JDK1.7和JDK1.8在HashMap的实现中的不同进行剖析比拟。

(1)、底层数据结构不同

在HashMap的put过程中,JDK1.7时是没有红黑树这一概念的,间接是进行的链表存储,在JDK1.8之后才引入了红黑树的概念,来优化存储和查找。

(2)、链表的插入方式不同

在HashMap向链表中插入元素的过程中,JDK1.7时是在表头节点插入的,JDK1.8之后是在尾节点插入的。

(3)、Hash(key)的计算形式不同

在Hash(key)的计算中,JDK1.7进行了九次扰乱,别离是四次位运算和五次异或运算,JDK1.8之后只进行了两次扰动。

(4)、扩容后数存储地位的计算形式不同

在扩容后对存储数据的重新排列上,JDK1.7是将所有数据的地位打乱,而后依据hash(key)进行从新的计算,而在JDK1.8之后是对原来的数据下标进行了两次for循环。计算出新下标地位只能是在原下标地位或者在原下标地位加上原容量地位。

好了,对于Map接口和HashMap的底层实现的过程,以及在面试中参考的外围问题就和大家剖析到这里!

点击关注,第一工夫理解华为云陈腐技术~