1.HashMap底层数据结构是什么?
答:HashMap底层数据结构是数组、链表和红黑树的组合,数组的主要作用是方便快速查找,时间复杂度是O(1),默认大小是16,数组的下标索引是通过key的hashcode计算的。数组元素叫做Node,当多个key的hashcode一致但key值不同时,单个Node会转化成链表,链表的查询复杂度是O(n),当链表的长度大于等于8并且数组的大小超过64时,链表会转化成红黑树,红黑树的查询复杂度是O(log(n)),简单来说,最坏的查询次数相当于红黑树的最大深度。
2.HashMap、TreeMap、LinkedHashMap三者有什么区别?
答:三者在特定的情况下,都会使用红黑树,底层的hash算法相同。在迭代的过程中,如果Map的数据结构被改动,都会报ConcurrentModificationException的异常。
不同点是,HashMap数据结构以数组为主,查询非常快,TreeMap数据结构以红黑树为主,利用了红黑树左小右大的特点,可以实现 key 的排序,LinkedHashMap在HashMap的基础上增加了链表的结构,实现了插入顺序访问和最少访问删除两种策略。由于三种Map底层数据结构的差别,导致了三者的使用场景有所区别。TreeMap适合需要根据key进行排序的场景,LinkedHashMap适合按照插入顺序访问或需要删除最少访问元素的场景,剩余场景使用HashMap。
3.说一下Map的hash算法?
答:源码中就是通过下面代码来计算hash的,首先计算出key的hashcode,因为key是Object,所以会根据key的不同类型进行hashcode的计算,接着计算h ^ (h >>> 16) ,这么做的好处是保证大多数场景下,算出来的hash值比较分散。
一般来说,hash值算出来之后,要计算当前key在数组中的索引下标位置,这里采用取模的方式,索引下标位置=hash值 % 数组大小,好处是可以保证计算出来的索引下标值均匀地分布在数组的各个索引位置上,但取模操作的计算相对是比较慢的,数学上有个公式,当b是2的幂次方时,a % b = a &(b-1),所以此处索引位置的计算公式我们可以更换为(n-1) & hash。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4.为什么不用key % 数组大小,而是需要用key的hash值 % 数组大小?
答:如果key是数字,直接用key % 数组大小是完全没有问题的,但key还有可能是字符串,是复杂对象,这时候用字符串或复杂对象 % 数组大小是不行的,所以需要先计算出key的hash值。
5.计算hash值时,为什么需要右移16位?
答:hash的算法是h ^ (h >>> 16),为了使计算出的hash值更分散,所以选择先将h无符号右移16位,然后再于h异或,就可以使h的高16位和低16位都参与计算,减少了碰撞的可能性。
6.为什么把取模操作换成了&操作?
答:key.hashCode()算出来的hash值还不是数组的索引下标,为了随机计算出索引的下标位置,还需要用hash值和数组大小进行取模,这样计算出来的索引下标分布会比较均匀。取模操作处理器计算比较慢,换成&操作,是有数学证明的支撑,提高了处理器处理速度。
7.为什么提倡数组大小是2的幂次方?
答:因为只有大小是2的幂次方时,才能使hash % n(数组大小) = (n-1) & hash成立。
8.HashMap是如何扩容的?
答:扩容有2个时机,一是在put时,发现数组为空,进行初始化时扩容,默认扩容大小为16。二是put成功后,发现现有数组大小大于扩容的门阀值时,进行扩容,扩容为老数组大小的2 倍。扩容的门阀是threshold,每次扩容时threshold都会被重新计算,门阀值等于数组的大小 * 影响因子(0.75)。新数组初始化之后,需要将老数组的值拷贝到新数组上,链表和红黑树都有自己拷贝的方法。
9.hash冲突时怎么办?
答:hash冲突指的是key值的hashcode计算相同,但key值不同的情况。如果桶中元素原本只有一个或已经是链表了,新增元素直接追加到链表尾部。如果桶中元素已经是链表,并且链表个数大于等于8时,此时有两种情况,如果此时数组大小小于64,数组再次扩容,链表不会转化成红黑树。如果数组大小大于64时,链表就会转化成红黑树。这里不仅仅判断链表个数是否大于等于8,还判断了数组大小。数组容量小于64没有立即转化的原因,猜测主要是因为红黑树占用的空间比链表大很多,转化也比较耗时,所以数组容量小的情况下冲突严重,可以先尝试扩容。
10.为什么链表个数大于等于8时,链表要转化成红黑树?
答:当链表个数太多时,遍历会比较耗时,转化成红黑树,可以使遍历的时间复杂度降低,但转化成红黑树,有空间和转化耗时的成本,通过泊松分布公式计算,正常情况下,链表个数出现8的概率不到千万分之一,所以说正常情况下,链表都不会转化成红黑树。这样设计的目的是为了防止非正常情况下(如hash算法不适用),当链表个数大于等于8时,仍然能够快速遍历。
11.红黑树什么时候转变成链表?
答:当节点的个数小于等于6时,红黑树会自动转化成链表,主要还是考虑红黑树的空间成本问题,当节点个数小于等于6时,遍历链表会很快,所以红黑树会重新变成链表。
12.HashMap在put时,如果数组中已经有了这个key,不想把value覆盖怎么办?取值时,如果得到的value是空时,想返回默认值怎么办?
答:如果数组有了key,但不想覆盖value,可以选择putIfAbsent方法,这个方法有个内置变量onlyIfAbsent,内置是true ,就不会覆盖,平时使用的put方法,内置onlyIfAbsent为false,是允许覆盖的。
取值时,如果为空,想返回默认值,可以使用getOrDefault方法,方法的第一个参数为key,第二个参数为返回的默认值,如map.getOrDefault(“2”,“0”),当map中没有key为2的值时,会默认返回0,而不是空。
13.LinkedHashMap中的LRU是什么意思,是如何实现的?
答:LRU(Least Recently Used,最近最少访问),在LinkedHashMap中也叫做最少访问删除策略,可以通过removeEldestEntry方法设定一定的策略,使得最少被访问的元素在适当的时机被删除。
在put方法执行的最后,LinkedHashMap会检查这种策略,如果满足策略,就删除头节点。LinkedHashMap在get的时候,都会把当前访问的节点,移动到链表的尾部,慢慢的,就会使头部的节点都是最少被访问的元素。