HashMap常见面试题

目录

HashMap自动扩容

相关面试题:

1、为什么默认初始容量要设置为16?

2、为什么加载因子是0.75?

3、哈希表的最小树形化容量为什么是64?

4、HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?

5、当两个对象的key的 hashCode 相等时会怎么样?

6、为什么改变链表插入方法?

7、新元素的位置?

8、hashMap和hashTable有啥区别?

补充:

参考文章:


HashMap是Java集合框架下的双列集合Map接口的一个子类实现。它的特点是:无序无索引、元素唯一、key可为null的双列集合。它的数据结构是:哈希表。

简单讲解下HashMap的原理:HashMap基于Hash算法,我们通过put(key,value)存储,get(key)来获取。当传入key时,HashMap会根据key.hashCode()计算出hash值,根据hash值将value保存在bucket里。当计算出的hash值相同时怎么办呢,我们称之为Hash冲突,HashMap的做法是用链表和红黑树存储相同hash值的value。当Hash冲突的个数比较少时,使用链表,否则使用红黑树。

HashMap使用了一个静态内部类Node< K, V>来存储数据。(1.7以前是Entry< K, V>)

在Java 8中,加入了一个常量TREEIFY_THRESHOLD=8,如果某个链表中的记录大于这个常量的话,HashMap会动态的使用一个专门的treemap实现来替换掉它。这样复杂度是O(logn),比链表的O(n)会好很多。当树中的元素小于6时,退化成链表。

HashMap自动扩容

HashMap的底层由数组 + 链表(红黑树)组成(1.7版本是数组+链表),数组的大小可以在构造方法时设置,默认大小为0,插入首个数据先扩容为16再插入数据,数组中每一个元素就是一个链表,jdk7之前链表中的元素采用头插法插入元素,jdk8之后采用尾插法插入元素,由于插入的元素越来越多,查找效率就变低了,所以满足某种条件时,链表会转换成红黑树。随着元素的增加,HashMap的数组会频繁扩容,如果构造时不赋予加载因子默认值,那么负载因子默认值为0.75,数组扩容的情况如下:

1:当添加某个元素后,数组中总的添加元素数大于了 数组长度 * 0.75(默认,也可自己设定),数组长度扩容为两倍。(如开始创建HashMap集合后,数组长度为16,临界值为16 * 0.75 = 12,当加入元素后元素个数超过12,数组长度扩容为32,临界值变为24)

2:在没有红黑树的条件下,添加元素后数组中某个链表的长度超过了8,数组会扩容为两倍。同时将新加入的元素存储在原元素位置+原数组长度的位置上。当数组长度达到64,链表长度达到了8,再向该链表中添加元素,则该链表会转换为红黑树。

例如:

开始创建HashMap集合后,假设添加的元素都在一个链表中,当链表中元素为8时,再在链表中添加一个元素,此时若数组中不存在红黑树,则数组会扩容为两倍变成32,假设此时链表元素排列不变,再在该链表中添加一个元素,数组长度再扩容两倍,变为64,新加入的元素存储在原元素再移动2次幂的位置(以0和1标记是在原位还是在右侧)。

假设此时链表元素排列还是不变,则此时链表中存在8个元素,这是HashMap链表元素数存在的最大值,此时,再向该链表中加入元素,满足了链表树化的两个条件(1:数组长度达到64, 2:该链表长度达到了8),该链表会转换为红黑树。

从基础到实践,一文带你看懂HashMap - 知乎

相关面试题:

1、为什么默认初始容量要设置为16

答:扩容频繁消耗CPU,容量过大造成内存浪费

我们都知道HashMap数组长度被设计成2的幂次方:数组下标索引的定位公式是:i = (n - 1) & hash,当初始化大小n是2的倍数时,(n - 1) & hash等价于n%hash。定位下标一般用取余法,但是与运算(&)比取余(%)运算效率高。因此,默认初始化大定义为2的幂,就是为了使用更高效的与运算。

那为什么初始容量不设计成4、8或者32....其实这是JDK设计者经过权衡之后得出的一个比较合理的数字,如果默认容量是8的话,当添加到第6个元素的时候就会触发扩容操作,扩容操作是非常消耗CPU的,32的话如果只添加少量元素则会浪费内存,因此设计成16是比较合适的,负载因子也是同理。

2、为什么加载因子是0.75

答:这个值是根据空间和时间,通过泊松分布算法得到的一个折中的值

在理想情况下,使用随机哈希码,节点出现的频率在 hash 桶中遵循泊松分布。0.0000006源码写的

对照桶中元素个数和概率的表,可以看到当用 0.75 作为加载因子时,桶中元素到达 8 个的时候,概率已经变得非常小,因此每个碰撞位置的链表长度超过 8 个是几乎不可能的,因此在链表节点到达 8 时才开始转化为红黑树。当然为了防止编程人员自己重新hashcode,让hash冲突增加,还是设置了红黑树的设定。

因为负载因为会使用在扩容上,0.5会使数组不停的扩容,扩容需要复制内容,牺牲了空间,换取了时间,而1.0会使红黑树一直处于修补树状态,会造成时间的浪费。而取舍是两边各一半,0.75

3、哈希表的最小树形化容量为什么是64

因为红黑树需要进行左旋,右旋,变色操作来保持平衡,所以当数组长度小于64,使用数组加链表比使用红黑树查询速度要更快、效率要更高

因为容量低于64时,哈希碰撞的机率比较大,而这个时候出现长链表的可能性会稍微大一些,这种原因下产生的长链表,我们应该优先选择扩容而避免不必要的树化。

4、HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?

答:对于 key 的 hashCode 做 hash 操作,无符号右移 16 位然后做异或运算。还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位异或运算效率是最高的。

5、当两个对象的key的 hashCode 相等时会怎么样?

答:会产生哈希碰撞。同时调用重写的equals方法比较key值,若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 且数组长度达到64就转换为红黑树存储。

5.1、什么是哈希碰撞,如何解决哈希碰撞?

答:只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。

5.2、如果两个键的 hashCode 相同,如何存储键值对?

答:通过 equals 比较内容是否相同。相同:则新的 value 覆盖之前的 value。不相同:则将新的键值对添加到哈希表中。

6、为什么改变链表插入方法?

在1.7中采用表头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题;在1.8中采用尾部插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

那为什么1.8改成尾插法了呢?主要是因为头插法在多线程环境下可能会导致两个节点互相引用,形成死循环

7、新元素的位置?

2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”即原索引加原来数组长度,1.7之前是重新计算hashcode

从基础到实践,一文带你看懂HashMap - 知乎

8、hashMap和hashTable有啥区别?

1、功能特性,hashTable是线程安全的,但是hashMap是线程非安全的,但是性能则相反 

2、结构特点,hashTable底层是数组+链表,hashMap底层是数组+链表+红黑树,hashMap底层数组长度是16,hashTable底层数组长度是11,,hashMap的key可以为Null,因为为null的时候默认把0存储进去,hashTable的key不能为Null 

3、存储和查询的算法不同,hashTable是将存储的key用哈希函数算出对应的哈希值,然后对应到数组的某个下标,但是hashMap对此又做了一个二次散列,减少了hash冲突的发生

补充:

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构,常用来存储和获取数据。

当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”。

位运算符:

1、 <<(算数左移)表示左移,不分正负数,低位补0

2、 >>(算数右移)表示右移,如果该数为正,则高位补0;如果该数为负,则高位补1。

3、 >>>(逻辑右移) 表示无符号右移,不分正负数,右移后高位补0。

4、<<<(逻辑左移)表示无符号左移,不分正负数,低位补0

逻辑运算符:

&是与运算符,与的规则是转换成二进制比较,同时为1得1,其余情况为0。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1。

|是或运算符,或的规则是转换成二进制比较,同时为0得0,其余情况为1。运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;。

^是异或运算符(把数据转换成二进制,然后按位进行运算),相同为0,不同为1。运算规则:0^0 = 0, 1^0 = 1, 0^1 = 1, 1^1 = 0。

参考文章:

从基础到实践,一文带你看懂HashMap - 知乎

关于hashmap_hashmap判断长度64_wendi➣的博客-CSDN博客

https://www.cnblogs.com/sunny226/p/17236318.html

&(与运算)、|(或运算)、^(异或运算)等运算符的解释与运用_或运算符_春风十里不如你9527的博客-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值