您的位置:首页 > 职场人生

别怕,遇到Map源码面试题这样答就对了

2020-04-03 18:20 1016 查看

众所周知,在面试大厂的过程中,HashMap源码是必问的,当然有些还会问TreeMap 和 LinkedHashMap。如果事先没有准备,当然会瞬间懵逼,因此在面试前建议大家抽出时间准备准备,很容易就能续写自己的高光时刻。

在开始之前,强烈推荐阅读这个多达两万字的HashMap源码分析文章,是的,你想要的,它都有!强化基础,HashMap源码全角度详细解析
别的废话就不多说了,下面开始见招拆招。

本文目录:

  • 3、HashMap、TreeMap、LinkedHashMap有什么相同点和不同点?
  • 4、HashMap源码里面是怎么解决hash冲突的?
  • 5、那你说说HashMap是怎么扩容的?
  • 6、那为什么链表个数大于等于8时,链表要转化成红黑树了?为什么不是6,不是10?
  • 7、红黑树什么时候转变成链表?
  • 8、HashMap 在 put 时,如果数组中已经有了这个 key,我不想把 value 覆盖怎么办?取值时,如果得到的 value 是空时,想返回默认值怎么办?
  • 9、删除数据时可以使用foreach进行删除吗?
  • 10、LinkedHashMap中的LRU是怎么实现的?
  • 11、为什么使用TreeMap时推荐把key实现 Comparable 接口?什么情况下不需要去实现这个接口呢?
  • 1、说一说 HashMap 底层数据结构

    HashMap 底层是数组 + 链表 + 红黑树的数据结构,数组的主要作用是方便快速查找,时间复杂度是 O(1),默认大小是16,数组的下标索引是通过 key 的 hashcode 计算出来的,数组元素叫做 Node,当多个 key 的 hashcode 一致,但 key 值不同时,单个 Node 就会转化成链表,链表的查询复杂度是 O(n),当链表的长度大于等于 8 并且数组的大小超过 64 时,链表就会转化成红黑树,红黑树的查询复杂度是 O(log(n)),简单来说,最坏的查询次数相当于红黑树的最大深度。

    2、那你说一下Map的hash算法

    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    可以得出key在数组中的位置公式:tab[(n - 1) & hash]

    如上代码是 HashMap 的hash 算法。

    这其实可以理解为一个数学问题,源码中就是通过以上代码来计算 hash 的,首先计算出 key 的 hashcode,因为 key 是 Object,所以会根据 key 的不同类型进行 hashcode 的计算,接着计算h ^ (h >>> 16) ,这么做的好处是使大多数场景下,算出来的 hash 值比较分散。

    一般来说,hash 值算出来之后,要计算当前 key 在数组中的索引下标位置时,可以采用取模的方式,就是索引下标位置 = hash 值 % 数组大小,这样做的好处,就是可以保证计算出来的索引下标值可以均匀的分布在数组的各个索引位置上,但取模操作对于处理器的计算是比较慢的,数学上有个公式,当 b 是 2 的幂次方时,a % b = a &(b-1),所以此处索引位置的计算公式我们可以更换为: (n-1) & hash。

    当你顺利的答出了上面这个问题后,可能面试官会接着追问如下四个问题,别怕,接着说就好了!

    1. 为什么不用 key对数组大小取余,而是需要用 key 的 hash 值对数组大小取余?

    如果 key 是数字,直接用 key % 数组大小是完全没有问题的,但我们的 key 还有可能是字符串,是复杂对象,这时候用 字符串或复杂对象 % 数组大小是不行的,所以需要先计算出 key 的 hash 值。

    2. 计算 hash 值时,为什么需要右移 16 位?

    hash 算法是 h ^ (h >>> 16),为了使计算出的 hash 值更分散,所以选择先将 h 无符号右移 16 位,然后再于 h 异或时,就能达到 h 的高 16 位和低 16 位都能参与计算,减少了碰撞的可能性。

    3. 为什么把取模操作换成了 & 操作?

    key.hashCode() 算出来的 hash 值还不是数组的索引下标,为了随机的计算出索引的下表位置,我们还会用 hash 值和数组大小进行取模,这样子计算出来的索引下标比较均匀分布。

    取模操作处理器计算比较慢,处理器对 & 操作就比较擅长,换成了 & 操作,是有数学上证明的支撑,为了提高了处理器处理的速度。

    4. 为什么提倡数组大小是 2 的幂次方?

    因为只有大小是 2 的幂次方时,才能使 hash 值 % n(数组大小) == (n-1) & hash 公式成立。

    3、HashMap、TreeMap、LinkedHashMap有什么相同点和不同点?

    3.1、首先是相同点:

    三者在特定的情况下,都会使用红黑树;
    底层的 hash 算法相同;
    在迭代的过程中,如果 Map 的数据结构被改动,都会报 ConcurrentModificationException 的错误。

    3.2、其次是不同点:

    HashMap数据结构以数组为主,查询非常快,TreeMap 数据结构以红黑树为主,利用了红黑树左小右大的特点,可以实现 key 的排序,LinkedHashMap 在 HashMap 的基础上增加了链表的结构,实现了插入顺序访问和最少访问删除两种策略;
    由于三种 Map 底层数据结构的差别,导致了三者的使用场景的不同:

    • TreeMap 适合需要根据 key 进行排序的场景;
    • LinkedHashMap 适合按照插入顺序访问,或需要删除最少访问元素的场景;
    • 剩余场景我们使用 HashMap 即可,我们工作中大部分场景基本都在使用 HashMap。

    4、HashMap源码里面是怎么解决hash冲突的?

    hash 冲突指的是 key 值的 hashcode 计算相同,但 key 值不同的情况。
    通过学习源码,可以知道如果桶中元素原本只有一个或已经是链表了,新增元素直接追加到链表尾部;如果桶中元素已经是链表,并且链表个数大于等于 8 时,此时有两种情况:

    • 如果此时数组大小小于 64,数组再次扩容,链表不会转化成红黑树;
    • 如果数组大小大于 64 时,链表就会转化成红黑树。

    这里不仅仅判断链表个数大于等于 8,还判断了数组大小,数组容量小于 64 没有立即转化的原因,猜测主要是因为红黑树占用的空间比链表大很多,转化也比较耗时,所以数组容量小的情况下冲突严重,可以先尝试扩容,通过扩容看看能不能解决根本的问题。

    5、那你说说HashMap是怎么扩容的?

    扩容需要满足一定的条件,主要是两种:

    1. put 时,发现数组为空,进行初始化扩容,默认扩容大小为 16;
    2. put 成功后,发现现有数组大小大于扩容的门阀值时,进行扩容,扩容为老数组大小的 2 倍。

    其次扩容的门阀是 threshold,每次扩容时 threshold 都会被重新计算,门阀值等于数组的大小 * 影响因子(0.75)。新数组初始化之后,需要将老数组的值拷贝到新数组上,链表和红黑树都有自己拷贝的方法。

    6、那为什么链表个数大于等于8时,链表要转化成红黑树了?为什么不是6,不是10?

    HashMap在源码的类注释里对为什么是8有过解释,是这样说的:通过泊松分布公式计算,正常情况下,链表个数出现 8 的概念不到千万分之一,所以说正常情况下,链表都不会转化成红黑树,这样设计的目的,是为了防止非正常情况下,比如 hash 算法出了问题时,导致链表个数轻易大于等于 8 时,仍然能够快速遍历。
    虽然成红黑树,可以使遍历的时间复杂度降低,但是也要考虑到转化成红黑树,有空间和转化耗时的成本,因此不能轻易的转化。

    7、红黑树什么时候转变成链表?

    当节点的个数小于等于 6 时,红黑树会自动转化成链表,主要还是考虑红黑树的空间成本问题,当节点个数小于等于 6 时,遍历链表也很快,所以红黑树会重新变成链表。

    8、HashMap 在 put 时,如果数组中已经有了这个 key,我不想把 value 覆盖怎么办?取值时,如果得到的 value 是空时,想返回默认值怎么办?

    如果数组有了 key,但不想覆盖 value ,可以选择 putIfAbsent 方法,这个方法有个内置变量onlyIfAbsent,内置是 true ,就不会覆盖,我们平时使用的 put 方法,内置 onlyIfAbsent 为 false,是允许覆盖的。
    取值时,如果为空,想返回默认值,可以使用 getOrDefault 方法,方法第一参数为 key,第二个参数为你想返回的默认值,如 map.getOrDefault(“2”,“0”),当 map 中没有 key 为 2 的值时,会默认返回 0,而不是空。

    9、删除数据时可以使用foreach进行删除吗?

    不可以,因为在for循环之前modCount会赋值给mc,但是循环之后,mouCount发生了变化,但是mc却是变化之前的值,这点和ArrayList删除数据类似,都是不可以用简单的foreach删除的。

    10、LinkedHashMap中的LRU是怎么实现的?

    首先LRU的英文全称是Least recently used,翻译过来就是最近最少访问,在 LinkedHashMap 中,也叫做最少访问删除策略,我们可以通过 removeEldestEntry 方法设定一定的策略,使最少被访问的元素,在适当的时机被删除,原理是在 put 方法执行的最后,LinkedHashMap 会去检查这种策略,如果满足策略,就删除头节点。

    保证头节点就是最少访问的元素的原理是:LinkedHashMap 在 get 的时候,都会把当前访问的节点,移动到链表的尾部,慢慢的,就会使头部的节点都是最少被访问的元素了。

    11、为什么使用TreeMap时推荐把key实现 Comparable 接口?什么情况下不需要去实现这个接口呢?

    这是因为 TreeMap 的底层就是通过排序来比较两个 key 的大小的,所以推荐 key 实现 Comparable 接口,是为了往你希望的排序顺序上发展。
    如果我们的key的类型本身已经实现了 Comparable 接口,比如String以及一些包装类型如Long、Double、Short 等等就不需要我们再去实现 Comparable 接口了。

    • 点赞 1
    • 收藏
    • 分享
    • 文章举报
    且听_风吟 发布了109 篇原创文章 · 获赞 190 · 访问量 24万+ 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: 
    相关文章推荐