【226期】面试问我,HashMap 的默认初始容量是多少,我该怎么说?

点击上方“Java精选”,选择“设为星标”

别问别人为什么,多问自己凭什么!

下方有惊喜,留言必回,有问必答!

每天 08:15 更新文章,每天进步一点点...

此文章包含了大部分HashMap有关的面试题,如有其它欢迎在评论区补充。

HashMap的一些基础知识:

e2436b3ddcaef6076717834ce4eb9536.png

前排提示:回答中加粗的部分为下个问题的诱导因素。

问:HashMap的默认初始容量是多少?

答:默认初始容量是16,且默认初始容量必须是2的次幂。

问:为什么默认初始容量必须是2的n次幂?若创建HashMap传入的initialCapacity不是2的次幂会发生什么?

答:因为(2的次幂数 - 1)的二进制形式表示都是1,这样在和经过异或运算的h进行按位与运算的时候才可以最多地保留其特性,减少产生哈希碰撞的概率,让数组空间均匀分配。

如果传入的initialCapacity不是2的次幂数,则HashMap会通过一通位移运算和或运算得到一个容量比传入的initialCapacity大的最小的2的次幂数,并将其作为HashMap的初始容量。例如传入7得到初始容量为8的HashMap,传入9得到初始容量为16的HashMap。

补充源码:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

问:插入新数据时如何计算其在数组的索引?

答:HashMap首先调用 hashCode() 方法,获取键key的 hashCode值h,然后对其进行高位运算:将h右移16位以取得h的高16位,与原h的低16位进行异或运算。再将返回的值与(数组长度-1)进行按位与运算,得到的便是新插入数据在table中的索引。

推荐下自己几个月熬夜整理的面试资料大全:

https://gitee.com/yoodb/eboo‍ks

问:为什么计算数组索引的时候要将哈希值与哈希值无符号右移16位进行异或运算?

答:如果当n即数组长度很小,假设是16的话,那么n-1的二进制表示为1111,这样的值和哈希值直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小时,这样就很容易造成哈希碰撞,所以这里把高低位都利用起来,从而解决这个问题。

问:当两个对象的hashCode值相等时会发生什么?

答:会产生哈希碰撞,若key值内容相同则替换旧的value;否则连接到链表后面,链表长度超过阈值8且table长度大于64就转换为红黑树。JKD8之前使用头插法,JDK8之后使用尾插法。

问:为什么Map桶中节点个数超过8才转为红黑树?

答:TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且查看源码时发现,链表长度达到8就转成红黑树,当长度降到6就转成普遙bin。

这也解释了为什么不是一开始就将其转换为 TreeNodes,而是需要一定节点数才转为 TreeNodes。其实就是权衡,在空间和时间中权衡。

hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机 hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机 hashCode算法下所有bin中节点的分布频率会遵循lambda为0.5的泊松分布,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是随便决定的,而是根据概率统计决定的。由此可见,发展将近30年的Java每一项改动和优化都是非常严谨和科学的。

也就是说:选择8因为符合泊松分布,超过8的时候,概率已经非常小了,所以才会选择8这个数字。

还有另外一种解释:红黑树的平均查找长度是log(n) ,如果长度为8,平均查找长度为log(8) = 3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6) = 2.6,虽然速度也比链表快,但是转化为树结构和生成树的时间并不会太短,且红黑树占用的内存比链表更大。

总之,树化阈值选择8是在时间和空间中找到一个最好的平衡点。

问:什么时候HashMap会扩容?怎样扩容?

答:当 HashMap中键值对的数量超过数组大小(数组长度)* loadFactor(负载因子)时,就会进行数组扩容。

扩容时先新建一个容量为原来两倍的HashMap,再进行rehash操作将原来的HashMap中的数据迁移到新的HashMap中去。

JDK8是使用新的rehash方法,即如果元素和原先数组长度进行按位与运算的结果为0,那么其迁移后的数组索引不变;否则迁移后的数组索引变为原数组长度+原数组索引。

推荐下自己做的 Spring boot 的实战项目:

https://gitee.com/yoodb/jing-xuan

问:为什么要reHash呢,直接复制过去不香么?

答:因为扩容后的重新计算索引的值也相应改变。

index = HashCode(Key) & (Length - 1)

因此如果直接复制过去的话,新扩容的一半数组都是空的,数据不够分散,会增大以后产生哈希碰撞的概率。

问:默认负载因子(加载因子)是多少?为什么设置为这个值?

答:默认负载因子为0.75。

负载因子不能设置太小很容易理解,因为设置太小的话会很容易进行扩容操作,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,且有大量开辟出的新空间未被使用,造成资源浪费。

不能将其设置太大是因为一般情况下哈希桶很难被填满,如果loadFactor设置太大的话,当size达到扩容阈值的时候,很有可能某些哈希桶下的链表长度会非常长,此时查询速度减慢,性能降低。

总的来说,负载因子太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75,是官方经过大量试验后给出的一个比较好的临界值。

问:table中用来存放元素的数组是什么类型的?

答:JKD8之前是Entry<K,V>类型,JDK8之后是Node<K,V>类型。其实只是换了一个名字,都实现了一样的接口:Map.Entry<K,V>。负责存储键值对数据。

问:HashMap是线程安全的吗?

答:HashMap不是线程安全的。

问:为什么HashMap不是线程安全的?

答:第一点是put时候多线程可能导致数据不一致;

第二点是JDK7中HashMap的get操作可能因为resize而引起死锁。

问:在什么情况下put时多线程会导致数据不一致?

答:假设有线程A和线程B,一开始线程A希望插入一个键值对到HashMap中,但是计算得到桶的索引坐标,获取到该桶里面的链表头结点后阻塞了,而此时线程B执行,成功地将键值对插入到了HashMap中。假设此时线程A被唤醒继续执行,而线程A保存的插入点正好是线程B插入元素的位置,如此一来ji就覆盖了线程B的插入记录,造成了数据不一致的现象。

问:JDK7中的HashMap为什么会出现死锁?

答:JDK7版本中的HashMap扩容时使用头插法,假设此时有元素一指向元素二的链表,当有两个线程使用HashMap扩容的时,若线程一在迁移元素时阻塞,但是已经将指针指向了对应的元素,线程二正常扩容,因为使用的是头插法,迁移元素后将元素二指向元素一。此时若线程一被唤醒,在现有基础上再次使用头插法,将元素一指向元素二,形成循环链表。若查询到此循环链表时,便形成了死锁。而JDK8版本中的HashMap在扩容时保证元素的顺序不发生改变,就不再形成死锁,但是注意此时HashMap还是线程不安全的。

问:使用数模方法解决HashMap线程不安全的问题?

答:使用线程安全的ConcurrentHashMap,

或者给HashMap加锁,但是会使效率变低。

问:ConcurrentHashMap是怎样保证其是线程安全的?

答:。。。。。。

小故事:在HashMap的继承关系中有一个很奇怪的现象,就是 HashMap已经继承了 AbstractMap,而 AbstractMap类实现了Map接口,但是HashMap又去实现了Map接囗。同样在 ArrayList中 LinkedList中都是这种结构。

据Java集合框架的创始人 Josh Bloch描述,这样的写法是一个失误。在Java集合框架中,类似这样的写法很多,最开始写Java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来了。

来源:知乎

https://www.zhihu.com/question/23084473

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

------ THE END ------

f77c2180e2dc8b54c4533d53b11c6874.png精品资料,超赞福利!2399930d5e784a3ae02a160220aefd8f.png

>Java精选面试题<
3000+ 道面试题在线刷,最新、最全 Java 面试题!

ccbf1b7128eaf25d07b7ad42b0266246.png

351419684a6796e507be1d3e4fd10ac7.png

期往精选  点击标题可跳转

【218期】玩转 Java 注解:元注解、内置注解、自定义注解的原理和实现

【219期】为什么代码规范要求 SQL 语句不要过多的 join?

【220期】面试官:一致性 Hash 是什么?在负载均衡中的应用

【221期】扔掉工具类,Mybatis 一个简单配置搞定数据加密解密!

【222期】一个适用于 Spring Boot 项目的轻量级 HTTP 客户端框架

【223期】面试官问:什么是 YAML?和 Spring Boot 有什么关系?

【224期】Java 字符串拼接五种方法的性能比较分析,从执行100次到90万次?

【225期】为什么数据库连接池不采用 IO 多路复用?

0d1778f8f17d01c1661096e33c325748.png技术交流群!e6bad5ca19f45f7968ce76a6b7f36ab0.png

最近有很多人问,有没有读者&异性交流群,你懂的!想知道如何加入。加入方式很简单,有兴趣的同学,只需要点击下方卡片,回复“加群”,即可免费加入交流群!

文章有帮助的话,在看,转发吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值