java面试题:Hashmap常见面试题

Hashmap详解

一.底层实现概述

  1. hashmap底层是使用数组+链表+红黑树实现的,初始容量为16,默认的负载因子为0.75,每次扩容为原来的两倍。
  2. 插入元素:首先将元素的hashcode值高16位与低16位异或得到hash值,之后将hash值与数组的长度-1进行与操作来得到桶的位置,如果发现hash冲突的话,那么就通过拉链来解决问题,如果没有冲突,那就new一个新的链表放进桶中,每次插入是尾插法。当链表长度大于8时,就会转化为红黑树。
  3. 查找元素:与插入相似,首先获取到hash值,然后找到桶,开始遍历链表或者红黑树,每次查找首先比较hashcode方法,如果hashcode相同,再比较equals方法。

二.细节实现

1. 初始容量的表现形式?为什么初始容量是16?
答:初始容量为16,但是表达形式是以位运算的形式写的,因为位运算比较快。设置为16的原因是因为是2的整数次幂,这样在hash的时候就可以减少碰撞。因为hash的操作是index = hash & (length -1),此时为15也就是1111,这样得到的结果其实就是hash的后几位,只要hash分布均匀,那么index也就是分步均匀的。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

2. 扩容机制是什么?什么时候扩容?如何扩容?
1)当容量达到(目前容量*负载因子)的大小时,就要开始扩容,这样做的目的是减少hash的碰撞
2)每次扩容都会新建一个两倍容量的数组,将原来的元素重新hash进去
3)为什么不是复制进去呢?因为数组的长度变了,hash的规则也就变了,hash是与数组的长度-1操作

3. hash函数是如何实现的?为什么要先用hashcode的16位进行异或操作?为什么与的是length-1?
答:
1)首先将元素hashcode值的高16位与低16位异或操作,作为hash值,然后与数组长度-1进行与操作,得到数组的位置。
2)当数组长度比较小的时候,长度的高位全部为0,那么此时与hashcode进行与操作对结果改变影响不大,这样会增大hash碰撞的几率。所以要先用hashcode的前后16为进行异或操作
3)因为hash的操作是index = hash & (length -1),此时为15也就是1111,这样得到的结果其实就是hash的后几位,只要hash分布均匀,那么index也就是分步均匀的。

三.hashmap的put,get方法原理

1. put过程:
1)首先将元素的hashcode高16位与低16位进行异或操作,再与length-1进行与操作,得到hash值,也就是桶的位置
2)之后查看桶中是否存有链表(红黑树),如果有的话就直接进行遍历,首先判断两个元素的hashcode是否相同,如果相同就再比较equals方法,如果为重复元素就进行覆盖,如果不是就直接尾插法插入链表(红黑树)
3)如果链表长度超过8,就自动转为红黑树
2. get过程:
get过程和put过程基本相似

四.常见面试问题

1. 为什么要转为红黑树?为什么不直接开始就使用红黑树?
1)因为当长度过长,遍历链表的时间也会原来越长,用红黑树可以减少遍历时间
2)如果一开始就使用红黑树,那么就要进行左旋,右旋,变色等操作,在元素个数较小的时候会消耗时间,并且遍历时间消耗与链表没什么区别

2.可不可以使用二叉树,不用红黑树?为什么阈值是8?

1)可以使用二叉树,但是使用二叉树可能会出现只有左子树或者右子树的情况,这样和链表没什么区别
2)阈值是8是因为泊松分布,单个hash槽中元素为8的概率小于百万分之一,所以选择7为分水岭,为7不做操作

3.一般使用什么作为key?
1) 一般使用String,Integer这种不可变类作为key,因为这样的话在对象创建之后hashcode就是定值, 并且这种类已经很好的实现了hashcode与equals方法的重写

4.为什么重写equals方法之后还要重写hashcode方法?
1)因为hashcode生成是一串定长的数字,当数据量很大时候,难免会出现不同对象hashcode相同的情况。也就是说hashcode相同,元素不一定相同,hashcoede不同,元素一定不同

5.hashmap如何解决线程不安全的情况?
1)使用HashTable 或 ConcurrentHashMap,但是推荐ConcurrentHashMap,因为hashtable加锁的方式很粗暴,加的是整个add方法,也就是锁住了整个数组,后者仅仅是锁住了一个node节点。

6.hashmap如何解决hash冲突,为什么hashmap中的链表需要转成红黑树?
1)hash冲突之后使用拉链解决,当链表的长度超过8会转为红黑树
2)因为当长度过长,遍历链表的时间也会原来越长,用红黑树可以减少遍历时间

7.hashmap什么时候会触发扩容?
1)容量达到负载因子就进行扩容

8.hashmap扩容时每个entry需要再计算一次hash吗?
1)在jdk7中是需要进行重新hash的,但是在jdk8中做了一定的优化
2)只需要看原来的hash值在扩容后新增的那一位是1还是0,如果是0的话索引没变,是1的话索引变成“原索引+oldCap
3)具体优化细节在下面有讲解

9. jdk1.8之前并发操作hashmap时为什么会有死循环的问题?
1)
1)因为jdk8之前插入是头插法,并且线程不安全的,可能两个线程同时进行扩容,这样就会导致一个链表中会出现循环链表的情况
2)jdk8中改成了尾插法,就不会出现这种问题。
3)具体详细解释可以看这篇博客:hashmap死循环详解

五.JDK8中对于扩容rehash的优化

  • 对于扩容机制我们知道,每次达到负载因子就扩容为原来的两倍,之后由于数组的长度变了,所以重新插入元素的时候需要重新进行hash,但是这样会非常的浪费时间,所以jdk8对rehash进行了优化

5.1举例

  • 假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:
    old capacity : 00010000
    new capacity : 00100000
  • 下标的计算方法,对于一个 Key,
  • 如原Hash值 key1 = 0001 1001 key2 = 0000 1001
    扩容前 hash & (length - 1) :
    key1 : 0001 1001 & 0000 1111 -> 0000 1001
    key2 : 0000 1001 & 0000 1111 -> 0000 1001

扩容后 hash & (length - 1) :
key1 : 0001 1001 & 0001 1111 -> 0001 1001
key2 : 0000 1001 & 0001 1111 -> 0000 1001

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,
只需要看原来的hash值在扩容后新增的那一位是1还是0,如果是0的话索引没变,是1的话索引变成“原索引+oldCap”

六.总结

面试官问到hashmap与hashtable的区别的时候,其实真正的区别就是两个:
1.线程安全与不安全
2.hashmap的键值可以为null

但是实际上回答这两点我觉得是不够的!
你还需要将hashtable加锁的方式说明一下,是用sy关键字锁住整个数组,所以推荐用ConcurrentHashMap进行线程安全的解决。

并且概述一下hashmap的底层实现。

如果面试问到请讲一讲hashmap的底层实现:
1.首先概述实现,存,取
2.之后介绍参数:初始容量,负载因子,红黑树的阈值
3.再介绍扩容机制,原理
4.最后介绍hash函数等

  • 7
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值