系列文章目录
第一章 ArrayList-Java八股面试(一)
第二章 HashMap-Java八股面试(二)
文章目录
前言
准备春招实习的小伙伴可以一起交流学习,动态每日更新算法题。
一、HashMap的底层数据结构
对于HashMap来说,jdk1.7和jdk1.8有所不同,这也是面试必问的题目,下面用图解方式来讲解。
1.1 HashMap的初始化
对于哈希表来说,任意一个对象包括数字,字母等等都会有哈希值这是Object带来的特性,既然有哈希值那么就可以根据哈希值以及特定的算法来算出桶下标,而这个桶下标就是索引位置。
当有不同的对象的桶下标算出来相同时,那么新来的对象会根据头插法(jdk1.7)或者尾插法(jdk1.8)的形式插入链表。
1.2 哈希表的扩容
当不断的往哈希表中加入对象之后哈希表会发生扩容。
当哈希表中的对象超过哈希表索引的3/4时(取决于加载因子),哈希表会自动扩容,那么对应的哈希值也会改变,进而会使得长链表进行拆分。
而加载因子的选择其实是根据空间和效率决定的,但是0.75也并不一定是最佳选择,还是需要看具体的数据来选择。只能说在随机分布的情况下,0.75有着很好的效果。
1.3 红黑树的转化
红黑树转化的阈值为8,当某个链表的长度大于8时,才会转化。
之所以需要转化为红黑树,具体可以联系两张图一起来看,当链表长度为8时,我们想要找到值为8的key需要循环遍历8次,而在红黑树中,我们只需要找3次。而红黑树的排列主要是根据哈希值来排序的,左小右大,并且红黑树是一颗二叉平衡树。
1.4 红黑树转化规则以及作用
对于红黑树来说,一般情况是不会发生的。因为存在二次哈希,所以即使有几十亿的对象生成,那么相同哈希的情况基本上不会超过6个,所以如上图所说概率仅为0.0000006。
转化条件如下:
- 链表长度大于8
- 哈希表的数组容量>=64
而退化条件如下:
- 链表长度小于等于6
- root、root.left、root.right、root.left.left为null时也会退化
二. HashMap的常见问题
2.1 二次哈希
综上所述,其实二次哈希还是数组容量尽量取2^n完全是为了计算方便,因为那样就可以用移位计算,这在计算机中是非常方便的。
而二次哈希也会让哈希分布变得十分均匀这是不可替代的。当然当数组容量取质数时也会变得十分均匀。
2.2 Put方法流程(jdk1.7/8区别)
由于链表不同的插入方法会带来以下问题,也是多线程下的问题
2.3 数据错乱(jdk1.7/8)
HashMap是线程不安全的。假如此刻存在两个线程1和2,当线程1取A元素时,还没取到值时,线程2优先完成了它对A元素值的更新,那么此时线程1就会取到错误的值。
因为Cpu是采用轮询制度的,线程1可能执行到一半,就轮到线程2并且线程2从零开始完成了所有任务。
有些人可能会说为什么线程1取A元素不能直接取到值,因为java代码和经过jvm编译后的代码是不同的put操作在java代码中只是一行,但是在底层可能会有多行操作,比如说先计算哈希值,再取值等等。
2.4 扩容死链(jdk1.7)
红色代表原哈希表,绿色代表扩容后的哈希表,当做数据迁移时会因为头插法b就会出现在a的上面。
假设在多线程的情况下
e代表当前需要迁移的元素,next代表当前需要迁移元素的下一个元素。
当经过第一轮迁移之后,线程2完成了它的操作,然后轮到线程1了。
当a完成了迁移之后,e成为了b元素代表下面是b需要迁移,但是b的next又是指向a元素。至于这里为什么会指向a元素是因为头插法将b.next=a而a.next =null;
因此造成了死链