HashMap的底层结构、原理、面试问题

1.为什么使用HashMap?

  • HashMap底层是数组加链表,主题部分是很长的数组。 不同的key存在不同的散列桶里,如果key的hash值相同会把相同hash值的key和value的放在同一个散列通,散列通里的键值对映射以链表的形式存在同一个散列通。
  • HashMap采用了数组加链表的数据结构,能查询和修改方面继承了数组的线性排查和链表 的寻址修改。
  • HashMap不需要考虑线程安全问题,所以HashMap相对于HashTable要快的多。
  • HashMap可以接受null值,而HashTable中不可以有null值,原因就是equals()方法需要对象,因为HashMap是后出的API经过处理才可以。

2.HashMap的工作原理是什么?

我们在存储键值对的时候,会先对key值调用hashcode方法,返回对应的hash值,目的是用来找到存储该对象的具体的桶位置,如果没有对应的桶就新建一个?每个桶的key的hash值是相同的。
具体的put过程
1.对key’值进行求hash值,然后再计算下标。
2.如果没有碰撞,直接放入桶中(空桶),(碰撞的意思就是计算出来的hash值相同,需要放在同一个桶中)。
3.如果碰撞了,用equals方法比较key值内容是否相同,相同就替代值,不同就链接到链表后面。
4.如果链表长度超过阀值8,就把链表转成红黑树,链表长度低于6,就不红黑树转换为链表。
5.如果所有的桶满了(容量16*加载因子0.75),就需要resize(扩容2倍后重排)

HashMap的底层结构、原理、面试问题_第1张图片

3.有什么方法可以减少碰撞?

**- 1.扰动函数可以减少碰撞,原理是他让内容不同的对象返回不同的hashcode值,这样就会少产生碰撞,也就是在数据结构中链表的结构少了,在取值时,会很少的调用equals方法,提高Map的性能,扰动hash方法的内部算法实现,目的是让不同的对象返回不同的hashcode。

  • 2.使用不可变的,声明为final的对象做键值,如String Integer。不可变使得能够缓存不同键的hashcode。这将提高整个回去对象的速度。因为String是final的,而且已经重写了equals()和hashCode()方法了。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。**

4.HashMap中的hash函数怎么实现的

return (n-1)&(h^(h>>>16));n是数组的初始化长度,h是hashcode返回的散列值也就是key的hash值。
简单来说就是:
1.高16bit不变,低16bit和高16bit做了一个异或运算
2.n-1&hash 得到下标。

5.拉链法导致的链表过深问题为什么不用二叉树代替,而选择红黑树?为什么不一直使用红黑树

之所以选择红黑树,是因为二叉树在特定情况下,会形成一条线的结构,这跟链表查询的一样了,造成查询很深的问题,遍历查询会变的非常的慢,二红黑树就是为了查询速度快,解决链表查询深度的问题,我们知道红黑树是属于平衡二叉树,但是为了保持平衡是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表的长度很短 的话,使用红黑树,反而会更慢。

6.说活你对红黑树的见解

HashMap的底层结构、原理、面试问题_第2张图片
1、没个节点非红即黑
2、根节点总是黑色的
3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
4、每个叶子的节点都是黑色的空节点(NULL)
5、从任意节点到其每个叶子的所有路径都包含相同的黑色节点。

7.解决hash碰撞还有那些办法?

开放定址法。
当冲突发生时,使用某种探查技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的地址。
按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。
下面给一个线性探查法的例子  
问题:已知一组关键字为(26,36,41,38,44,15,68,12,06,51),用除余法构造散列函数,用线性探查法解决冲突构造这组关键字的散列表。
解答:为了减少冲突,通常令装填因子α由除余法因子是13的散列函数计算出的上述关键字序列的散列地址为(0,10,2,12,5,2,3,12,6,12)。
前5个关键字插入时,其相应的地址均为开放地址,故将它们直接插入T[0],T[10),T[2],T[12]和T[5]中。
当插入第6个关键字15时,其散列地址2(即h(15)=15%13=2)已被关键字41(15和41互为同义词)占用。故探查h1=(2+1)%13=3,此地址开放,所以将15放入T[3]中。
当插入第7个关键字68时,其散列地址3已被非同义词15先占用,故将其插入到T[4]中。
当插入第8个关键字12时,散列地址12已被同义词38占用,故探查hl=(12+1)%13=0,而T[0]亦被26占用,再探查h2=(12+2)%13=1,此地址开放,可将12插入其中。
类似地,第9个关键字06直接插入T[6]中;而最后一个关键字51插人时,因探查的地址12,0,1,…,6均非空,故51插入T[7]中。

8.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

默认的负载因子大小为0.75,也就是说,当一个map填满75%的时候,和其他集合类一样,将会创建原来HashMap大小的两倍的数组,来重新调整map的大小,并将原来的对象放入新的数组,这个过程叫做rehashing,因为它调用hash方法找到新的位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置。

9.从新调账HashMap大小存在什么问题

  • 当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小,他们会同时调整大小。在调整大小的过程中,存储在链表中的元素次序会反过来,因为移动到新的桶的位置的时候,HashMap并不会将元素放在链表 的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就死循环了(多线程情况下不使用HashMap)
  • 为什么多线程情况下会导致死循环,他是怎么发生的 ?
    Hash Map的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的j几率逐渐提高。这个时候,HashMap需要扩容它的长度,也就是进行resize,
    1、扩容:创建一个新的数组,长度是原来数组长度的两倍。
    2、ReHash:遍历原来的数组,把所有的entry从新hash到新的数组。链表元素会原链表的元素放在头部。
    (详细的为什么会发生死循环链接: https://coolshell.cn/articles/9606.html )

你可能感兴趣的:(随记,整理一点点)