别问别人为什么,多问自己凭什么!
下方有惊喜,留言必回,有问必答!
每天 08:15 更新文章,每天进步一点点...
此文章包含了大部分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/ebooks
问:为什么计算数组索引的时候要将哈希值与哈希值无符号右移16位进行异或运算?答:如果当n即数组长度很小,假设是16的话,那么n-1的二进制表示为1111,这样的值和哈希值直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小时,这样就很容易造成哈希碰撞,所以这里把高低位都利用起来,从而解决这个问题。答:会产生哈希碰撞,若key值内容相同则替换旧的value;否则连接到链表后面,链表长度超过阈值8且table长度大于64就转换为红黑树。JKD8之前使用头插法,JDK8之后使用尾插法。答: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中键值对的数量超过数组大小(数组长度)* loadFactor(负载因子)时,就会进行数组扩容。扩容时先新建一个容量为原来两倍的HashMap,再进行rehash操作将原来的HashMap中的数据迁移到新的HashMap中去。JDK8是使用新的rehash方法,即如果元素和原先数组长度进行按位与运算的结果为0,那么其迁移后的数组索引不变;否则迁移后的数组索引变为原数组长度+原数组索引。推荐下自己做的 Spring boot 的实战项目:
https://gitee.com/yoodb/jing-xuan
index = HashCode(Key) & (Length - 1)
因此如果直接复制过去的话,新扩容的一半数组都是空的,数据不够分散,会增大以后产生哈希碰撞的概率。问:默认负载因子(加载因子)是多少?为什么设置为这个值?负载因子不能设置太小很容易理解,因为设置太小的话会很容易进行扩容操作,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,且有大量开辟出的新空间未被使用,造成资源浪费。不能将其设置太大是因为一般情况下哈希桶很难被填满,如果loadFactor设置太大的话,当size达到扩容阈值的时候,很有可能某些哈希桶下的链表长度会非常长,此时查询速度减慢,性能降低。总的来说,负载因子太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75,是官方经过大量试验后给出的一个比较好的临界值。答:JKD8之前是Entry<K,V>类型,JDK8之后是Node<K,V>类型。其实只是换了一个名字,都实现了一样的接口:Map.Entry<K,V>。负责存储键值对数据。第二点是JDK7中HashMap的get操作可能因为resize而引起死锁。答:假设有线程A和线程B,一开始线程A希望插入一个键值对到HashMap中,但是计算得到桶的索引坐标,获取到该桶里面的链表头结点后阻塞了,而此时线程B执行,成功地将键值对插入到了HashMap中。假设此时线程A被唤醒继续执行,而线程A保存的插入点正好是线程B插入元素的位置,如此一来ji就覆盖了线程B的插入记录,造成了数据不一致的现象。答:JDK7版本中的HashMap扩容时使用头插法,假设此时有元素一指向元素二的链表,当有两个线程使用HashMap扩容的时,若线程一在迁移元素时阻塞,但是已经将指针指向了对应的元素,线程二正常扩容,因为使用的是头插法,迁移元素后将元素二指向元素一。此时若线程一被唤醒,在现有基础上再次使用头插法,将元素一指向元素二,形成循环链表。若查询到此循环链表时,便形成了死锁。而JDK8版本中的HashMap在扩容时保证元素的顺序不发生改变,就不再形成死锁,但是注意此时HashMap还是线程不安全的。问:使用数模方法解决HashMap线程不安全的问题?答:使用线程安全的ConcurrentHashMap,问:ConcurrentHashMap是怎样保证其是线程安全的?小故事:在HashMap的继承关系中有一个很奇怪的现象,就是 HashMap已经继承了 AbstractMap,而 AbstractMap类实现了Map接口,但是HashMap又去实现了Map接囗。同样在 ArrayList中 LinkedList中都是这种结构。据Java集合框架的创始人 Josh Bloch描述,这样的写法是一个失误。在Java集合框架中,类似这样的写法很多,最开始写Java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来了。来源:知乎
https://www.zhihu.com/question/23084473
公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!精品资料,超赞福利!
3000+ 道面试题在线刷,最新、最全 Java 面试题!
技术交流群!
最近有很多人问,有没有读者&异性交流群,你懂的!想知道如何加入。加入方式很简单,有兴趣的同学,只需要点击下方卡片,回复“加群”,即可免费加入交流群!