其他面试题:
目录
2.HashMap的put(key,value)和get(key)过程
7.为什么jdk1.8之后要对hashmap进行了红黑树的改动?
8.jdk1.8对hashmap除了对红黑树的改动还有哪些改动呢?
9.扩容是在原有的hashmap中扩还是新建一个hashmap对象?如果是后者?旧hashmap怎么处理?
1.请你说说java中hashmap的原理
在jdk1.7前,hashMap底层是由数组和链表实现的,key值会通过hashCode()方法计算出哈希值并映射到数组的槽位,当多个key值映射到同一个槽位时,则会以链表形式放到同一个槽位。
Jdk1.8时得到优化,链表长度超过8时将链表转换为红黑树,提高查询性能。(延伸→为什么要转换为红黑树?跳转至第七题)
Hashmap的默认初始容量为 16,负载因子为 0.75。也就是说,当存储的元素数量超过 16x0.75=12 个时, Hashap会触发扩容操作,容量x2并重新分配元素位置。这种扩容是比较耗时的操作,频繁扩容会影响性能。
2.HashMap的put(key,value)和get(key)过程
在put时,
我们将key/value传给put,它调用hashCode计算hash从而得到插槽位置,
并判断这个插槽位置是否已经被占用,没有的话就直接存入,
有的话就遍历这个位置的链表或红黑树看看是否有相同的键,
有的话就更新这个键的值,没有的话就直接存入到链表或数据结构;
然后hashmap开始判断链表的长度是否超过阈值,
是的话就转换为红黑树进行存储;
之后hashmap检查键值对的数量与数组长度比值是否达到负载因子,是的话就进行扩容。
在get时,
我们将key传给get,它调用hashCode计算hash从而得到插槽位置,
进一步调用equals方法确定键值对,从而取出value值。
3.在使用hashmap时,有哪些提升性能的技巧?
1.合理设置hashmap的初始容量和负载因子,尽量减少扩容操作
2.尽量让hash值均匀分布,避免哈希碰撞
3.选择合适的数据类型,使用合适的数据结构:LinkedHashMap(插入顺序存储)、ConcurrentHashMap(高并发线程安全)、TreeMap (可以排序)
4.什么是哈希碰撞?怎么解决哈希碰撞?
Hash 碰撞是指不同的数据通过哈希算法计算后得到相同的哈希值,映射到了哈希表的同一个位置,发生”碰撞”解决方案:
1.拉链法(链地址法): 将哈希表的每一个槽的位置变成一个链表,将哈希值相同的键存储到同一个链表中
2.开放寻址法: 如果出现碰撞,就寻找哈希表下一个可用的位置.
3.再哈希法(双重哈希): 在出现碰撞时,使用第二个哈希函数计算新的索引位置, 减少碰撞的概率
5.谈一谈hashmap的扩容操作?
hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,,则会触发扩容,扩容分为两个步骤:
- 第1步是对哈希表长度的扩展(2倍)
- 第2步是将旧哈希表中的数据放到新的哈希表中。
因为hashmap的初始容量是16,转换为2进制就是有四位(2的四次方),每次扩容都是乘以2,也就是位数+1,每次扩容的时候元素的hash值也会发生变化,这时候只需要看hash值新增的那个位是0还是1即可,如果是1那该元素在新hashmap的新位置为:旧位置+旧hashmap容量。
因此,hashmap扩容之后,部分元素的位置会发生变化,但不需要去重新计算哈希值,但即使如此hashmap的扩容操作还是一个比较耗时的操作,我们要尽可能减少hashmap的扩容。
延伸→怎么减少扩容?→(设置大一点的初始容量和负载因子)
延伸→初始容量和负载因子越大越好吗?(问题6)
延伸→扩容是在原有的hashmap中扩还是新建一个hashmap对象?如果是后者?旧hashmap怎么处理?(问题9)
6.hashmap的初始容量和负载因子越大越好吗?
不是的,初始容量设置的大一点确实是可以减少扩容操作,但同时带来的问题就是内存空间的浪费,而负载因子大一点确实也能减少扩容操作,但太大也会影响性能,最极端的例子就是当负载因子为1时,虽然可以减少扩容次数,提高内存利用率,但它增加了哈希冲突的可能性。冲突过多会导致桶内链表或红黑树变长,降低查找插入和删除的效率。因此,负载因子1.0会使得时间复杂度劣化为 0(n),不利于 Hashap的高效运行。
7.为什么jdk1.8之后要对hashmap进行了红黑树的改动?
在 JDK 1.8之前, Hashmap 使用链表来解决哈希冲突。当哈希冲突较多时,链表中的元素增多,查找、插入和删除的时间复杂度从 O(1) 退化为 O(n)。
因此在JDK1.8引入红黑树,将链表长度超过一定阈值(默认 8)时的链表转换为红黑树,避免性能急剧下降。当链表长度降到6以下时红黑树会重新退化为链表,保持简单高效。
红黑树是一种平衡二叉搜索树,插入、删除、查找操作的时间复杂度为 Ò(log n),在元素多的情况下远优于链表的 O(n)。
延伸→既然红黑树效率这么高,那为什么不直接用红黑树就好了,还要用链表呢?
因为红黑树节点的大小是普通节点大小的两倍,所以为了节省内存空间不会直接只用红黑树,只有当节点到达一定数量才会转成红黑树,这里定义的是 8。
为什么是 8 呢?通过计算表明这个长度是时间和空间的平衡点。
8.jdk1.8对hashmap除了对红黑树的改动还有哪些改动呢?
1.优化了哈希值的计算
2.将头插法改成了尾插法
3.优化了扩容机制,在jdk1.8之前,是将所有元素重新计算哈希值,然后根据哈希值重新寻找插入的哈希桶,在idk1.8之后是如果是旧数组中对应的新数组的位置,就直接复制到新数组当中,如果不对应,就计算元素的哈希值,并找到插入的哈希桶
9.扩容是在原有的hashmap中扩还是新建一个hashmap对象?如果是后者?旧hashmap怎么处理?
在Java中的HashMap
实现中,当HashMap需要扩容时,它会创建一个新的HashMap实例,并将旧HashMap中的所有元素重新计算哈希值后插入到新的HashMap中。这个过程称为rehashing。
具体来说,当HashMap中的元素数量超过其容量(capacity)和加载因子(load factor)的乘积时,HashMap会进行扩容。扩容操作通常包括以下几个步骤:
- 创建一个新的HashMap实例,其容量通常是原HashMap容量的两倍。
- 遍历原HashMap中的所有元素。
- 对于每个元素,根据新的容量重新计算其哈希值。(jdk1.8前)
- 将元素插入到新的HashMap中。
- 将原HashMap中的所有引用指向新的HashMap。
这个过程中,旧的HashMap实例不会被修改,而是被新的HashMap实例替代。这样做的好处是可以减少在扩容过程中对HashMap操作的影响,因为元素的插入和查找操作不需要在扩容的同时进行。不过,这也意味着在扩容过程中,HashMap可能会暂时占用更多的内存,因为旧的HashMap实例和新的HashMap实例会同时存在,直到所有的元素都被迁移到新的HashMap中。
总而言之就是:是新建一个容量为旧容量两倍的hashmap,然后把旧hashmap的元素迁移过去,旧hashmap之后会被JVM的垃圾回收机制回收。
(这里可能会被面试官延伸到JVM和垃圾回收机制那边)
10.hashmap有什么缺点呢?
hashmap比较明显的两个缺点就是扩容操作开销大和线程不安全。
hashmap不是线程安全的,hashmap在多线程会存在下面的问题:
JDK 1.7 HashMap 采用数组 +链表的数据结构,多线程背景下,在数组扩容的时候,存在 Entry 链死循环和数据丢失问题。
JDK 1.8 HashMap 采用数组 + 链表+ 红黑二叉树的数据结构,优化了 1.7 中数组扩容的方案,解决了Entry 链死循环和数据丢失问题。但是多线程背景下,put 方法存在数据覆盖的问题。
如果要保证线程安全,可以使用ConurrentHashMap这个数据结构。
(这里可能可以把面试官引入ConurrentHashMap这个知识点来)
标签:扩容,10,面试题,java,hashmap,黑树,链表,哈希,HashMap From: https://blog.csdn.net/csdn3043663729/article/details/144164981