HashMap相关面试题
详见语雀:
https://www.yuque.com/xingsujtpw/gmrbgh/pv0u9pvyb36lcgs2?singleDoc#
HashMap相关面试题
4.3面试题-说一下HashMap的实现原理?
#我的表述 采用键值对, 键是Object对象, 而值又是数组索引下标映射对象 --上述是错误的 #自己重新表述一下 **HashMap的数据结构: 底层使用hash表数据结构,即数组 + 链表或 数组 + 红黑树
- 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况。
a. 如果key相同,则覆盖原始值;
b. 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
面试官追问:HashMap的jdk1.7和jdk1.8有什么区别
#我的表述 HashMap的jdk1.7只有链表, 而jdk1.8不仅有链表, 还有红黑树- JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
- jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表
4.4面试题-HashMap的put方法的具体流程
#我的表述 第一, 实例化HashMap 第二, 使用key的hashCode重新hash计算当前对象的元素在数组中的下标 第三, 存储值时, 会出现hash相同的key两种情况, 若相同则覆盖原来的值, 若不同即hash冲突, 则将其存储到对应的链表中 #自己重新表述一下 **第一, 判断数组是否为空(懒加载)
第二, 计算数组下标 : (n - 1) & hash
第三, 判断该槽位有没有数据
第四, 槽位为空, 直接put进去
第五, 槽位不为空, 判断key是否相同
第六, 判断是不是红黑树节点
第七, 是链表, 遍历链表, 尾部插入
第八, 判断size是否超过阈值
业务场景:用HashMap缓存菜品列表,避免重复查数据库
需求:运营后台频繁查看"起售中的菜品列表",每次都要查数据库,很慢。用HashMap做缓存,第一次查完存起来,后面直接从缓存取。
DishService中
@ServicepublicclassDishService{// 缓存:key是状态(1=起售),value是该状态下的菜品列表privateHashMap<Integer,List<Dish>>dishCache=newHashMap<>();publicList<Dish>getDishesByStatus(Integerstatus){// 1. 先从缓存取List<Dish>dishList=dishCache.get(status);if(dishList==null){// 2. 缓存没有,查数据库dishList=dishMapper.getByStatus(status);// 3. 放入缓存 ← ✅这里触发HashMap的put流程dishCache.put(status,dishList);log.info("从数据库查询菜品,状态:{}",status);}else{log.info("从缓存获取菜品,状态:{}",status);}returndishList;}}4.5面试题-讲一讲HashMap的扩容机制
#我的表述 数组不为空扩容 红黑树拆分后扩容 链表超过8,数组长度大于64扩容- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值(数组长度 * 0.75)
- 每次扩容的时候,都是扩容之前容量的2倍;
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
- 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
- 如果是红黑树,走红黑树的添加,把树拆分成两个小树。拆分后如果小树节点少于6个,就变回链表,否则保持红黑树
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
4.6面试题-hashMap的寻址算法
#我的表述 获取值时, 直接找到hash值对应的下标, 在进一步判断key是否相同, 从而找到对应的值 -- 部分正确第一,首先获取key的hashCode值,然后右移16位 异或运算 原来的hashCode值, 主要作用是使原来的hash值更加均匀, 减少hash冲突
第二,计算数组下标,用
(n-1) & hash代替hash % n。因为位与运算比取模快,但要求数组长度必须是2的幂次方。
关于hash值的其他面试题:为何HashMap的数组长度一定是2的次幂?
#我的表述 无, 没想到- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
4.7面试题-hashmap在1.7情况下的多线程死循环问题
#我的表述 hashMap1.8之前采用的是拉链法, 拉链法的意思是数组 + 链表, 若遇到hash冲突, 则把新增的数据添加到链表中 --只能算做了解jdk1.7, 而不是答案所问因为jdk1.7的hashMap在数组进行扩容的时候, 链表插入采用头插法, 在进行数据迁移的过程中, 则可能导致死循环.
(下面例子不懂问AI)比如说,现在有一个线程A和一个线程B读取hashMap数据进行数组扩容
- A和B中数组里数据的一个原链表是
A → B- 本来线程1先扩容, 但是线程2抢先完成扩容,用头插法把链表变成
B → A——即B.next = A, A = null- 线程1继续迁移时,它先处理A,把A放入新链表
- 然后处理B,发现
B.next指向了A(被线程2改的)- 把B插入链表头部后,
B.next还是指向A- 然后线程1又去处理A,A的next指向B
- 形成循环引用
A ↔ B,程序卡死
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。
4.8面试题-HashSet与HashMap的区别
#我的表述 从底层数据结构看 hashSet是集合 hashMap是数组+链表/数组+红黑树 从数据效率看 hashMap的查找效率是O(1), 而HashSete则是O(n) HashMap和HashSet的插入删除在有数组下标索引时的时间复杂度都是O(1), 如果索引未知, 则时间复杂度都是O(n) 从内存大小看 HashSet存储的是不重复的元素, 而HashMap则是键值对, 会出现采用hashCode的hash值计算数组下标索引, 而数组下标索引相同但key不同, 会插入到链表/红黑树中, 所以会出现更多的数据 从线程安全看 HashSet和HashMap都是非线程安全 --上述表述错误(1)HashSet实现了Set接口, 仅存储对象; HashMap实现了Map接口, 存储的是键值对.
(2)HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认为Object对象. 所以HashSet也不允许出现重复值, (后面这句话理解麻烦, 用AI进行业务场景描述), 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
4.9面试题-HashTable与HashMap的区别
难易程度:☆☆
出现频率:☆☆
#我的表述 HashTable实现了Array接口, 存储的是数组, HashMap实现了Map接口, 存储键值对 HashTable的底层是HashMap, HashTable封装了一系列HashMap方法, 用HashMap进行存储, 其hashMap的key键存储, 用value值存储元素值, 所以hashTable存储不允许出现重复值. -- 上述我的表述错误主要区别:
| 区别 | HashTable | HashMap |
|---|---|---|
| 数据结构 | 数组+链表 | 数组+链表+红黑树 |
| 是否可以为null | Key和value都不能为null | 可以为null |
| hash算法 | HashTable直接用key.hashCode()取模 | HashMap先高16位和低16位异或,再用位运算,分布更均匀 |
| 扩容方式 | 当前容量翻倍 +1 | 当前容量翻倍 |
| 线程安全 | 同步(synchronized)的,线程安全 | 非线程安全 |
在实际开发中不建议使用HashTable,在多线程环境下可以使用ConcurrentHashMap类
实际场景上面表格表述 = 如下表述:
第一, 数据结构, HashTable是数组 + 链表, 而HashMap是数组 + 链表 / 数组 + 红黑树
第二, 是否可以为null, HashTable的key和value不可以为null, 而HashMap的键值对可以为null
第三, Hash算法, HashTable是用hashCode计算, 再取模, 而HashMap是高16位与低16位异或, 然后再位运算, 使哈希分布更均匀.
第四, 数组扩容, HashTable是原先翻倍+1, 而HashMap是原先翻倍
第五, 线程安全, HashTable是同步synchronized, 是线程安全的, 而HashMap是非线程安全的.
所以在实际开发中不建议使用HashTable, 而在多线程环境下使用ConcurrentHashMap.
