高频题目
(1)装填因子,负载因子,加载因子为什么是0.75?
- 本质上是时间和空间的问题。
- 比如装填因子设置为1:空间利用率得到了很大的满足,很容易碰撞,产生链表->查询效率低。
- 再比如装填因子设置为0.5:碰撞的概率低,扩容,产生链表的几率低,查询效率高,空间利用率太低。
- 经过大量的实验论证使用0.75是比较平衡的。
(2)HashMap 的长度为什么是 2 的幂次方?
- 采用二进制位操作 &,相对于%能够提高运算效率
- hash%length==hash&(length-1) 的前提(即使用&代替%的前提) 是 length 是 2 的 n 次方;
- 可以减少hash冲突;
- 扩容移动元素会比较方便;
(3)HashMap 和 Hashtable 的区别?
- 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!因为HashTable 性能不行);
- 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
- 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
补充一个面试场景:
-
面试官:对HashMap了解吗?
-
我:了解过
-
面试官:你先说一下啥叫Hash吧?
-
我:Hash 就是通过特定的算法将一个任意长度的输入映射成固定长度的输出。
-
面试官:那这个过程会不会遇到一些问题?
-
我:是会有一些问题的,比如不同的value值通过我们的Hash算法可能会得到相同的key,也就是我们常说的Hash冲突问题。
-
面试官:那有办法避免Hash冲突吗?
-
我:… 目前根据我的了解好像不能
-
面试官:如果让你考虑一个Hash算法你会从哪些方面进行考虑?
-
我:相同的输入可以得到相同的输出,而且这个过程的效率也要得到保证;对输入做出轻微的改动,输出肯定要改变的;不能根据输出的key逆推出输入的value,然后数据分布尽量均匀,也就是尽量散列。
-
面试官:行,我们回归主题,HashMap的数据接口大致讲一下?
-
我:分为jdk 1.8前后,1.8之前是数组加链表,1.8之后是数组加链表加红黑树;我接下来都以1.8为例,它的数据单元是Node结构,里面有key,value,hash,还有指向下一节点的next指针。
脑海中浮现出当时看过的源码 -
面试官:HashMap的初始长度是多少?
-
我:16
-
面试官:那这个初始长度为16的散列表(数组)是啥时候创建的?
-
我:实际执行 put 方法的时候创建的(有点懒加载的感觉)
-
面试官:负载因子知道吗?默认是多少?有什么用?
-
我:默认是0.75,主要是用来计算扩容阈值的,比如总共是16,那16*0.75=12,达到12就要扩容了。
-
面试官:链表转红黑树要达到什么条件?
-
我:首先链表长度要达到8,其次当前散列表数组的长度要达到64,否则会优先考虑扩容而不是转成红黑树。
-
面试官:嗯嗯,node对象有一个hash字段,这个hash是key的hashcode()返回值吗?
-
我:不是的,是经历了一个扰动函数,简单说就是讲hashcode()的返回值无符号右移16位,然后与原值进行异或运算。
-
面试官:为什么要这么做?
-
我: 增大随机性,混合高低位增大随机性,减少碰撞概率。
-
面试官: 讲一下HashMap put的过程吧!
-
我:嗯,前面讲到经过扰动函数的到hash,然后实际插入数组下标还要 hash&(len-1)计算一下,如果是第一次进行put操作会调用扩容初始化数组,如果有冲突了向链表中插入数据,如果冲突十分严重了(链表长度达到8,数组长度超过64了,已经转红黑树了),向红黑树插入数据。再详细说一下就是,put时看一下有没有hash冲突没有直接new Node()放在数组中,有冲突看一下key是不是相同的,相同进行覆盖,不相同遍历一下尾插,然后判断是否需要转红黑树扩容啥的。
-
面试官:能讲一下插入红黑树的过程吗?
-
我:这方面没有太深入了解,然后我把红黑树的特点说了一下(是平衡二叉树,由黑色节点和红色节点组成,根节点是黑色,一个红色节点有两个黑色子节点)
-
面试官:为啥1.8的时候加入了链表转红黑树?
-
我:因为链表长度太长后查询太慢时间复杂度是O(n),红黑树查找效率会快很多时间复杂度是O(logn)
-
面试官:HashMap 扩容后数组怎么迁移的?
-
我: 对于null,和没有发生hash冲突的就没啥好说的了,重点是迁移链表,在jdk1.8后就不需要重新计算key的位置了,根据 hash&oldcap(扩容前数组长度)如果是0不用位置不变,如果是1加原数组长度即为新的位置,相当于原来的链表被拆成两个部分,在这个过程中是有四个指针,第一个指向是结果是0链表的开始节点(也可以叫做低位节点),第二个指向结果是0的链表的结束节点(也可以叫做高位节点),
第三个指向是结果是1链表的开始节点(也可以叫做低位节点),第四个指向结果是1的链表的结束节点(也可以叫做高位节点),然后为0的指向原本的数组位置,为1的指向新位置(原来位置加原来数组长度)。然后再说一下红黑树,红黑树内部节点并没有舍弃next字段,其实内部还维护了一份链表,只是查询的时候不去用,可以利用这个链表快速进行拆分,过程和链表基本一致,就是要注意小于6时红黑树要转回链表。
先写到这里,回来写一下ConcurrentHashMap 的面试题。