赞
踩
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也就是分步均匀的。
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死循环详解
扩容后 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函数等
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。