经典的HashMap面试点及底层原理(看这一篇就够了)

HashMap

Java 7 中 HashMap

使用的是数组 + 单向链表

  • 获取数据的 hashCode 时用了一堆的异或

  • Java 7 是数据结构中哈希表非常平铺直叙的实现(后面会对HashTable做一个简单的介绍)

  • 潜在的安全隐患

  • 使用的是头插法(后面会讲到两种插入法的区别)

  • 非线程安全

  • 潜在的安全隐患(Tomcat 邮件组发布):

Tomcat 中可以利用http发送 成千上万个哈希值一样的数据,形成了一个长度非常恐怖的链表,造成服务器性能下降

临时解决方法:通过设定 http 请求参数限制 ;

  • 非常容易碰到死锁(环形链表):造成你的线上系统 CPU 百分百,当你查看每个线程栈的时候,你会发现 hashMap 的 get() 一直在死循环

这也是程序员自己的锅,在 java 7 中,由于链表是头插法,HashMap 进行扩容时,重新计算hash值,链表节点发生变化,有些节点哈希到了新的地方,顺序反了,多线程环境下,查找线程 get() 发现都不是,形成了环,造成CPU百分百
经典的HashMap面试点及底层原理(看这一篇就够了)_第1张图片
经典的HashMap面试点及底层原理(看这一篇就够了)_第2张图片
后面的就不放上来了,为什么会形成死锁(环形链表)具体参考:

https://coolshell.cn/articles/9606.html

JDK 1.8 之后

  • 源码比jdk 1.7 膨胀了将近一倍:

因为它在里面自己实现了一个 TreeMap(将链表转换为红黑树)

  • 使用的是数组 + 单向链表 + 二叉树
  • 使用的是尾插法(后面会讲到两种插入法的区别)
  • 非线程安全

链表转化为二叉树的前提条件为:

  • 当集合的总长度大于64,才有如果链表的长度大于 8 ,这时才转换为二叉树
  • 如果二叉树中的内容长度小于 6 ,转换回链表

经典的HashMap面试点及底层原理(看这一篇就够了)_第3张图片

面试考点:

1、HashMap初始化时存放的对象 hashCode 值时很大的,而 HashMap 的初始长度是很小的(size = 16),它是怎么进行存储的呢?

很多人对HashMap中元素存放时到底是按哈希值 % 数组的长度 == 得到的就是数组的下标还是使用 hash & (length - 1) 很模糊 ,下面进行解答:
答案是使用的是:    hash  &  (length - 1)

原因:

如果 用 hash % 数组长度有以下缺点

  • 哈希值取值区间为 java 中的 int (2 的 -31次幂 ~ 2 的 31 次幂),当哈希值为负数时还需要转换为整数
  • 较慢( 取余的本质就是不断进行除法)
hash & (length - 1)

& : 按位与

因为 2 的 n 次幂 - 1 转换为二进制都是 1111. . . .等等
经典的HashMap面试点及底层原理(看这一篇就够了)_第4张图片

如 1010 转化为十进制为 10
经典的HashMap面试点及底层原理(看这一篇就够了)_第5张图片

2、链表长度为什么一定要到 8 才转化为红黑树呢?

因为它符合 泊松分布,链表超过 8 的时候概率就已经非常小了

什么是 泊松分布 ?

  • Poisson分布,是一种统计与概率学里常见到的离散概率分布(详细点击超链接)

3、负载因子为什么一定时 0.75 呢?

在HashMap源码中注释提到了:

一般来说,默认的负载因子在时间和空间复杂度上提供了一个很好的折中,如果负载因子高的话虽然减少了空间的浪费,但是增加了查找的消耗

4、HashMap 的数组大小为什么一定要是 2 的幂?

只有它的长度是 2 的 n 次方的时候,我们对它减一操作才能拿到全部为 1 的值,这样对它进行按位与就能够非常快速的用位运算拿到数组的下标,并且它的分布还是均匀的

按位与比取余快很多,采用按位与运算来提高效率,这就是为什么桶的个数要是2的整数次幂

说白了就是效率高、速度快!

5、HashMap中初始化元素是多少个?

首先这个问题你要知道HashMap 在创建出来时并不会初始化 16 个哈希桶,而是调用 put() 方法添加数据时才会创建;

6、什么时候会触发扩容?

很多人在这个问题上会掉进陷阱里。我会这样问,是数组中占用位置个数大于扩容因子的时候还是HashMap元素总数大于扩容因子的时候需要扩容?

如果对HashMap理解不透彻,这里很容易就答错了。这里HashMap中元素总个数达到阈值时就会扩容。很多人可能会疑问,为什么是总个数,而不是数组占用个数呢?

想象一下这个情况:假设有12个元素都落到了数组的同一个位置(当然现实情况这种机率非常非常小,几乎没有),数组只占用了一个位置, 那么为什么要扩容呢,还有那么多位置没用呢? 其实这里之所以要扩容,是有一个隐含的逻辑,如果元素总个数大于阈值,而数组占用位置没达到阈值,说明这些元素在当前长度下,分布是“不均匀”的,扩容是为了让其分布“更均匀”

7、当我们元素hash碰撞过多导致的链表过深,为什么不用二叉查找树代替而选择红黑树?为什么不一直使用红黑树?

之所以选择红黑树是为了解决二叉查找树的缺陷:二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成层次很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋、右旋、变色这些操作来保持平衡。引入红黑树就是为了查找数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。
经典的HashMap面试点及底层原理(看这一篇就够了)_第6张图片

8、resize 之后高位和低位的分配?

  • 要么跟原来的索引值相等
  • 要么就是在原来的 hashCode(如1111:15) 的高位上加1(11111:31)

最后介绍下什么是哈希表及Java 8 HashMap其他改进:

哈希表简介

  • 核心是基于哈希值和桶和链表
  • O(1) 的平均查找、插入、删除时间
  • 致命缺陷是哈希值的碰撞(collision)
  • 默认容量:11(质数为宜)
  • 对修改 Hashtable 内部共享数据的方法添加了 synchronized 关键字,保证线程安全

哈希桶的 时间复杂度O(1) 和数据规模无关(也就是不管你哈希桶有多少个,我查询一个元素永远都只需要查询一次)

Java 8 HashMap的改进

  • 数组 + 链表/红黑树
  • 扩容时插入顺序的改进
  • 函数方法
    • forEach
    • compute系列
  • Map的新API
    • merge
    • replace

总结

这篇文章只是对常见问题的一些总结,面试官可能还会继续问,HashMap是不是线程安全的呀,为什么不是线程安全的呀,在什么情况下会发生死锁呀,哪个结构是线程安全的呀等等问题,这就涉及到操作系统层面线程,死锁的考察了。

后面我会对 concurrentHashMap 进行详细介绍,最后祝大家找到一份满意的工作!!

你可能感兴趣的:(HashMap)