文章目录
- 标题1、HashMap 了解吗?平时在什么地方使用过它呢?(说明:发现没有,我喜欢问使用场景,希望大家也是能够思考使用场景的,因为掌握了这个,你说话更加有说服力)
- 标题2、HashMap 底层数据结构说一下?(指导:直接说最新的即可,不需要去对比以前的版本,因为面试官也听烦了,另外在说的时候,为了你语言的严谨,一定要强调下是哪个JDK版本的哈)
- 标题3、为什么用红黑树呢?用平衡二叉树不可以吗?或者你讲一讲他们各自的优缺点吗?
- 标题4、为什么选择 8 之后转为红黑树呢?另外链表转为红黑树之后,还会继续转为链表吗?
- 标题5、简单描述下 put 的流程?可以说一下JDK为了效率更快,在 put 的时候,做了哪些优化不?
- 标题6、多线程情况下,put 是线程安全的吗?可以简单举个例子,说一下哪里不安全吗?
- 标题7、如果我想要让 hashmap 变成线程安全的,你觉得可以怎么做?(有时候会扯到 concurrentHashMap,不过咱们这里先不追击这个)
- 标题8、头插法会导致死循环,那你觉得在以前的版本中,为啥会使用头插法呢?
- 标题9、那我们再说一说 HashMap 的扩容吧,什么时候会扩容呢?你觉得负载因子为啥选择 0.75 呢?
- 标题10、频繁扩容会导致效率比较低下,那你觉得在平时,在实际的开发场景中,可以怎么优化来避免频繁扩容呢?
- 标题11、一个场景题:只存60个键值对,需要设置初始化容量吗?设置的话设置多少初始化容量比较好呢?
标题1、HashMap 了解吗?平时在什么地方使用过它呢?(说明:发现没有,我喜欢问使用场景,希望大家也是能够思考使用场景的,因为掌握了这个,你说话更加有说服力)
了解,比如通过名称指定配置信息,建立对象与对象的映射关系,设置缓存从而可以避免频繁查询数据库和文件系统,提高效率,使用hashmap存储请求路由规则,根据路由规则以及请求参数查找对应的服务器列表。
标题2、HashMap 底层数据结构说一下?(指导:直接说最新的即可,不需要去对比以前的版本,因为面试官也听烦了,另外在说的时候,为了你语言的严谨,一定要强调下是哪个JDK版本的哈)
JDK >= 1.8的时候是数组+链表+红黑树,Hash值产生碰撞时,链表长度>8会由链表转换成红黑树(将链表转换成红黑树前判断,如果当前数组长度小于64,那么选择先数组扩容,而不是转换成红黑树),当红黑树节点<6的时候,会由红黑树转换成链表,就是二者的性能临界点。
HashMap的默认初始化大小为16,之后每次扩充容量变为原来的2倍,并且HashMap总是使用2的幂作为哈希表的大小。
标题3、为什么用红黑树呢?用平衡二叉树不可以吗?或者你讲一讲他们各自的优缺点吗?
HashMap底层使用红黑树的主要原因是为了在桶中存在大量键值对时提高查找效率。当桶中的链表长度较长时,查找效率可能会变得比较低,这时可以将链表转换为红黑树,以便在O(log n)的时间复杂度内查找键值对。
平衡二叉树的初衷是解决普通二叉树在频繁的插入,删除等动态更新的情况下出现的时间复杂度退化的问题。
红黑树与avl树对比:
红黑树是近似平衡的。相比于avl树,检索时效率差不多,都是通过平衡来二分查找,但是对于插入删除等操作提高很多。红黑树不像avl树一样追求绝对平衡,它允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价很大,所以效率不如红黑树。
红黑树高度只比高度平衡的AVL树的高度(log2 n)仅仅大一倍,性能上却好很多。
AVL树是一种高度平衡的二叉树,所以查找的非常高,但是有利就有弊,为了维持这种高度平衡,要付出更多代价,每次插入删除都要调整,比较复杂耗时。对于有频繁的插入和删除操作的数据集合,使用AVL树的代价就有点高了。红黑树的插入,删除,查找各种操作性能都比较稳定。
标题4、为什么选择 8 之后转为红黑树呢?另外链表转为红黑树之后,还会继续转为链表吗?
防止链表过长,从而导致查询效率过低,红黑树有自平衡的特点,可以防止不平衡的情况发生,时间复杂度由n变为lgn。
更多还是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,转化为红黑树更多是一种保底政策。
红黑树的个数被删为6的时候会回退回链表。
标题5、简单描述下 put 的流程?可以说一下JDK为了效率更快,在 put 的时候,做了哪些优化不?
HashMap提供了put方法添加元素,putval方法只是给put方法调用的一个方法,如果数组为空或者长度为0就扩容,不为空的话,如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不同就判断p是否是一个树节点是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
将元素添加进去,如果不是就遍历链表插入(插入链表的尾部),当链表长度大于阈值并且数组长度超过64就会执行链表转红黑树的操作,否则就只是对数组扩容。
jdk8在扩容时计算数组元素下标时做了优化:
jdk1.7:
重新计算每个元素的哈希值
static int indexFor(int h, int length) {// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";return h & (length-1);}
jdk1.8:
在扩容时没有像JDK1.7那样,而是通过高位运算e.hash & oldCap
,来确定元素是否需要移动。
do {next = e.next;// resize前后,桶的位置未变化// 尾插法插入链表if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}// resize后新产生的桶// 尾插法插入链表else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}
} while ((e = next) != null);
//不同移动
key1.hash = 10 0000 1010
oldCap = 16 0001 0000
//需要移动
key2.hash = 10 0001 0001
oldCap = 16 0001 0000
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HWq0wW2m-1686556971002)(学习规划.assets/image-20230612113807172.png)]
标题6、多线程情况下,put 是线程安全的吗?可以简单举个例子,说一下哪里不安全吗?
不安全,如果两个线程同时对一个HashMap操作,一个线程在其中添加或删除元素,另一个正在遍历该HashMap,这种情况可能导致一个线程看到不一致的状态。
import java.util.HashMap;public class SynchronizedHashMapExample {public static void main(String[] args) throws InterruptedException {HashMap<String, Integer> map = new HashMap<>();// 添加元素map.put("A", 1);map.put("B", 2);map.put("C", 3);// 创建锁对象Object lock = new Object();// 第一个线程遍历HashMapThread thread1 = new Thread(() -> {synchronized (lock) {for (String key : map.keySet()) {System.out.println(key + ": " + map.get(key));}}});// 第二个线程在遍历过程中删除一个元素Thread thread2 = new Thread(() -> {synchronized (lock) {map.remove("B");}});thread1.start();thread2.start();thread1.join();thread2.join();}
}
标题7、如果我想要让 hashmap 变成线程安全的,你觉得可以怎么做?(有时候会扯到 concurrentHashMap,不过咱们这里先不追击这个)
jdk7死循环例子:
5张图讲明白JDK1.7下的HashMap死循环(原理+实战) - 知乎 (zhihu.com)
关键点:线程2先扩容完毕,此时布局是这样的,但是线程1却还在按照只有两个桶的时候的布局来进行扩容,读取数据的时候却又是按照T2已经扩容完毕的数据来进行。
可以使用同步机制:可以使用Java中的synchronized关键字来保证多个线程对HashMap的同步访问。在使用synchronized关键字时,需要确保所有对HashMap的访问都在同步代码块中完成,以保证线程安全。
标题8、头插法会导致死循环,那你觉得在以前的版本中,为啥会使用头插法呢?
头插法速度最快,找到数组位置就直接找到插入位置,无需再遍历链表,但是可能会造成多线程同时扩容出现死循环。
标题9、那我们再说一说 HashMap 的扩容吧,什么时候会扩容呢?你觉得负载因子为啥选择 0.75 呢?
- table为null的时候扩容
- 数组中的元素个数达到阈值扩容
- 数组中的链表个数达到8以上,并且数组的大小小于64
0.75是为了触发扩容,减少冲突发生的概率,加载因子很大,扩容条件就会苛刻,hash碰撞概率变高,每个链表长度都很长,查询速度变慢,太小又会导致扩容频率变高,,内存消耗变大。
标题10、频繁扩容会导致效率比较低下,那你觉得在平时,在实际的开发场景中,可以怎么优化来避免频繁扩容呢?
- 根据业务,给hashmap初始大小一个合理的值减少扩容次数。
- 选择合适的负载因此控制hashmap的扩容频率
- 避免hashmap中使用复杂对象作为键,这会增加哈希算法的复杂度,如果使用确保其类覆盖hashcode方法和equals方法。
标题11、一个场景题:只存60个键值对,需要设置初始化容量吗?设置的话设置多少初始化容量比较好呢?
需要,否则就需要多次扩容,取2的幂次方,也就是128即可,因为2的幂次高位是1其余位置是0,当进行hash运算时,(n-1)&hash,n-1就是高位是0,低位都是1,这样&完之后,结果各个位置的取值取决于hash,如果不是2的整数次幂必然会有0位,0与任何&肯定为0,会造成更多的哈希冲突。
回答指导:上面这些问题,不应该需要记住!你需要理解,先理解大概=》看一下源码怎么做的,反正回答出上面这些问题,基本稳