《老板,我想进大厂》-- HashMap指北(上)

《老板,我想进大厂》-- HashMap指北(上)_第1张图片

01.写在前面

Hello,大家好,我是公子照谏。

今天是《老板,我想进大厂》系列的第一篇文章。在这个系列中,我会分享在前段时间中,我和我的小伙伴在面试中遇到的各种问题。希望我们的经历可以带给大家收获。

那么废话就不多说了,开始这个系列的第一篇文章《HashMap》指北。

02.面试开始

本文代码基于:Java 1.8_192,Java 1.7
如果代码有出入,请以作者的版本为准。

聊一下HashMap的结构

HashMap的底层是散列表,简单的说就是数组加链表的组合结构。
《老板,我想进大厂》-- HashMap指北(上)_第2张图片
HashMap在进行put/get操作时,通过对Entry的Key进行Hash操作来存取数据,如果不同的Key得到了相同的Hash值,则会在该位置上构建链表。

在Java 8之后,对散列表的结构进行了优化,单个链表长度超过8之后,会转变为红黑树。
《老板,我想进大厂》-- HashMap指北(上)_第3张图片
对链表的查找需要遍历链表,时间复杂度为O(N),而对红黑树的查找,时间复杂度为O(lgN)。

HashMap的初始长度,是不变的吗?

HashMap中数组的长度是16。当元素数量达到了阈值的时候,HashMap会进行扩容。阈值的大小由加载因子和当前容量决定。

需要注意的是,这里是HashMap中存储的元素数量,即便数组只使用了一个节点,其余元素均在这个节点上构建链表,HashMap也会进行扩容,为的是将链表上的元素均匀分布到数组上,加快访问速度。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final float DEFAULT_LOAD_FACTOR = 0.75f;

扩容可以分为两步:

  • 创建一个新的数组,长度为原来的2倍
  • 遍历所有元素,进行rehash操作,确定位置

为什么要rehash?

HashMap的put和get操作都要依赖于对Key进行Hash的值,这个值是确定元素在数组中的下标,通过下面的公式计算:

index = e.hash & (newCap - 1);

当数组的大小发生改变时,index的计算结果也会不同,如果不进行rehash操作,那么已经存入到HashMap中的元素,将无法获取。

Java 8之前是真正意义上的rehash,对key重新进行Hash,而在Java 8之后,HashMap的内部类Node会存储Hash值,只是利用 Hash & length 来确定数组下标。

Java 8 对HashMap还做了哪些改变?

Java 8之前,如果多个元素定位到数组的同一个位置,则会在链表的头部进行插入,Java 8之后改为尾部插入。先来看下Java 7的源码:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry e : table) {
        while(null != e) {
            Entry next = e.next;// 代码1
            if (rehash){
                e.hash = null == e.key ? 0 : hash(e.key);         
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];// 代码2
            newTable[i] = e;// 代码3
            e = next;// 代码4
        }
    }
}

假设有如下HashMap,A,B两个线程同时put:
《老板,我想进大厂》-- HashMap指北(上)_第4张图片
put完成后,线程A抢到CPU资源,进行resize。遍历到3的,且执行到代码1,此时e = 3,next = 2;随后线程B抢到CPU资源,完成resize操作后,HashMap如下:
《老板,我想进大厂》-- HashMap指北(上)_第5张图片
这时,CPU资源交还给线程A,线程A继续resize操作。
要注意的是,线程B已经修改过2和3的指向,此时的关系是2->3,而线程A拿到的关系是3->2。
此时的链表指向关系已经乱了,那么结果也不会好到哪里去。

线程A执行完代码2~4后,e= 2,进入第二次循环,继续执行代码1,上面已经提到过,线程B修改了2和3的指向关系,所以此时拿到的结果是:e = 2,next = 3。另外,需要注意的是,线程A,已经将3插入到
《老板,我想进大厂》-- HashMap指北(上)_第6张图片
接着执行到代码2,2指向3,执行代码3,将2赋值到数组上,执行代码4,e = 3。
《老板,我想进大厂》-- HashMap指北(上)_第7张图片
接着进入第三次循环,此时3的next为null,略过前面的内容,执行到代码2,此时的该位置上的元素是2,那么3指向2,执行代码3,将3赋值到数组上,执行代码4,next为null,跳出循环,此时,我们得到一个结果:
《老板,我想进大厂》-- HashMap指北(上)_第8张图片
至此,“大功告成”,环形链表形成。

尾插法是如何避免这种情况的?

如果是尾插法,那么情况应该是这样的:
《老板,我想进大厂》-- HashMap指北(上)_第9张图片
依旧是线程A在执行在进入循环后丢失CPU资源,此时e = 1,next = 2,此时线程B完成resize操作。
《老板,我想进大厂》-- HashMap指北(上)_第10张图片
线程A重新获取CPU资源,执行完第一次循环后,1被放在了下标0,此时e = 2;
《老板,我想进大厂》-- HashMap指北(上)_第11张图片
进入第2次循环,e = 2, next = 3,执行完代码后,2被放在了数组上,e = 3;
《老板,我想进大厂》-- HashMap指北(上)_第12张图片
进入第3次循环,3正常放置即可,此时HashMap如下:
《老板,我想进大厂》-- HashMap指北(上)_第13张图片

03.写在后面

原计划是打算一口气写完,我遇到的关于HashMap的全部面试题的,不过写到现在发现已经写了很多了,而且,关于头插法形成环形链表这块,如果不是“人肉逻辑机”的话,还是要自己手动画一下的,这样比较容易理解。我们来回顾下,这期都写了什么:

  • HashMap的结构
  • HashMap扩容(关于长度和rehash那段)
  • Java 8对HashMap的优化(红黑树,尾插法)
  • Java 7中HashMap形成环形链表(死锁问题)

虽然只写了4个点,但是也已经涵盖了关于HashMap的大部分内容,下期主要讲两个方面:

  • HashMap的线程安全问题(Java 8中会出现)
  • Java 8为什么选择红黑树,而不是AVL树或者B树

04.求关注啊

我会持续不断的更新这个系列的,毕竟我在4~5月经历了20+面试,录音,录屏超过25个小时,收集150+最新各大厂面试题的男人。

如果觉得有收获的话希望大家多多关注,点赞,我是公子照谏,一个逆势跳槽的程序员。
《老板,我想进大厂》-- HashMap指北(上)_第14张图片

你可能感兴趣的:(老板,我想进大)