HashMap面试题

目录

1、ArrayList插入的时候,index是自增的,省去了很多麻烦,插入效率比较高?为什么HashMap没有这么做?

2、jdk1.7时HashMap为什么用头插法?

3、HashMap的get操作和put是怎样实现的?

4、HashMap的key可以为NULL吗?链表是单向还是双向?

5、put操作的返回是什么?

6、为什么HashMap初始化一定要是2的幂次方?

7、HashMap扩容的逻辑是怎样的?

8、jdk1.7和jdk1.8 HashMap的区别有哪些?


jdk1.8之前的内部结构-HashMap

1、ArrayList插入的时候,index是自增的,省去了很多麻烦,插入效率比较高?为什么HashMap没有这么做?

答:因为这样index自增的话插入删除效率高,但是查找起来效率很低。

HashMap<String, Stirng> hashMap = new HashMap<>();
hashMap.put("123, "2");   //key-----key.hashcode()--------1420572488-----1420572488 % table.length-----index-----0-table.length-1

2、jdk1.7时HashMap为什么用头插法?

答:最近访问过的数据下次大概率会再次访问,把刚访问过的元素放在链表最前面可以直接被查询到,减少查找次数。虽然头插法和尾插法都需要遍历,但头插法不一定需要遍历到尾部,而尾插法需要遍历到尾部。

3、HashMap的get操作和put是怎样实现的?

get(key){
    int hashcode = key.hashcode();
    int index = hashcode % table.length;
    
}
put(key,value) {
    int hashcode = key.hashcode();
    int index = hashcode % table.length;
    Entry entry = new Entry(key,value);
    //table[index] = entry;  //table[index]的链表原来为空的情况下,需要把数组中的这个索引改成put进去的这个entry
    // table[index]的链表原来不空时
    table[index] = new Entry(key,value,table[index]); //new Entry(key,value,table[index])表示插入到链表头节点,赋值给table[index]表示将数组中的entry替换为新插入的entry
    size++;
}

put操作流程:①计算key的哈希值,从而计算出新entry在table数组中的索引;

②遍历索引位置的链表,如果key相同,则覆盖,并返回覆盖的value;若无相同key则往下执行;

③将新entry的指针指向索引位置的链表的头节点,并将数组的索引位置改成新entry,成功实现头插法。


get操作流程:①先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。

②通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个索引上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着key和单向链表上的每一个节点的key进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的key和参数key进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

4、HashMap的key可以为NULL吗?链表是单向还是双向?

可以,单向链表,因为双向链表需要多存一个指针,占用内存

5、put操作的返回是什么?

返回之前被覆盖的value,没有被覆盖则返回null。

6、为什么HashMap初始化一定要是2的幂次方?

HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash%length,但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1  实际就是n个1;
例如长度为9时候,3&(9-1)=0  2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3  2&(8-1)=2 ,不同位置上,不碰撞;

当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

7、HashMap扩容的逻辑是怎样的?

扩容条件:

1、 存放新值的时候当前已有元素的个数必须大于等于阈值(阈值 = 容量 x 加载因子;加载因子小则冲突概率小,内存浪费大)

2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)

扩容结果:

链表中的数据变为倒序,HashMap容量变为原来的2倍,阈值也变为原来的2倍,加载因子不变。

扩容步骤:

①判断是否符合扩容条件,符合则扩容

②建立newtable,将oldtable的哈希桶中的第一个entry的next指针指向newtable[i],然后将该entry赋值给newtable[i],之后不断循环。核心代码如下:

//获取索引
int i = indexFor(e.hash), newCapacity);

e.next = newTable[i];

newTable[i] = e;

//继续向下遍历
e = next

③遍历完成后,table数组引用指向了newTable,完成扩容。

高并发场景下扩容带来的问题:

①数据丢失。高并发场景下,新增对象丢失可能的原因有:并发时赋值被覆盖;新表被覆盖;迁移丢失;已遍历区间新增元素会丢失。

②死链问题。两个线程A和B,执行transfer方法。虽然newTable是局部变量,但是原先table中的Entry链表是共享的。产生问题的根源是Entry的next被并发修改。这可能导致:对象丢失;两个对象互链;对象自己互链。

jdk7:先扩容,后新增元素;
jdk8:先新增元素,后扩容;

8、jdk1.7和jdk1.8 HashMap的区别有哪些?

参考链接:jdk1.7与jdk1.8中HashMap区别(面试最详细版)_PANDA博客-CSDN博客

8.1. 结构

jdk1.7:数组+链表。

jdk1.8:数组+链表+红黑树结构。

改动原因:红黑树本质上是一棵二叉查找树,它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。加快检索速率。 而链表的查找复杂度为O(n)。

8.2. put方法

jdk1.7:采用头插法。

jdk1.8:尾插法。

改动原因:jdk1.7因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。

使用头插法会改变链表的上的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了, Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。

8.3. 哈希值计算方法

jdk1.7:对哈希值的计算直接使用key的hashCode值。

jdk1.8:采用key的hashCode异或上key的hashCode进行无符号右移16位的结果。

改动原因:避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀。


8.4. 扩容

jdk1.7:扩容时会颠倒原链表的顺序;在元素插入前检测是否需要扩容。

jdk1.8: 扩容时会保持原链表的顺序;而且1.8是在元素插入后检测是否需要扩容。

改动原因:①保持链表顺序避免死链问题;②jdk1.7采用头插法,扩容后,计算hash,只需要插入链表头部就行。而jdk1.8采用尾插法,如果先扩容,扩容后需要遍历一遍,再找到尾部进行插入。

8.5. 扩容策略

jdk1.7:只要不小于阈值就直接扩容2倍。

jdk1.8: 当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数达到8就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。

改动原因:利用红黑树提升查找效率和扩容效率。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值