HashMap的面试题你能回答几个?

本文深入探讨了HashMap的数据结构实现方式,解析了其get过程、容量初始化原则及扩容机制,并解释了为何长度总是2的幂。
1.HashMap用什么数据结构实现的?

答:数组。什么样的数组?答:Node[] table这样的。Node是什么?答:看下图:


也就是说这个数组每个元素都是个单向链表。


2.HashMap的get过程是?

答:先得到key的hash值,再把这个hash值与length-1按位与(取余),得到table数组的下标。取出这个下标值的key,与传入的key比较,如果相同那就是这个了。如果不同呢,那就沿着这个单向链表向后找,直到找到或找到结束也找不到。

等等,这个hash值与length-1按位与就是等同取余么?为什么?

答:是的,因为这个length是有特点的,是2的n次方,length-1就是二进制全部为1。那么一个数的二进制按位与这些全为1的位,就会得到与这些全为1的位对齐的另一个数,如(15&6=1111&0110=6=0110)。而这个数正好是取2的n次的余数(6%15=6=15&6)。 至于为什么这个length是2的n次方,下面的问题会回答 。贴下源码:


3.HashMap初始化传入的容量参数的值就是HashMap实际分配的空间么?

答:不是。那是什么呢?是比传入容量参数值大的最小的2的n次方,比如传入6,实际分配8。看下计算容量的源码。虽然这里源码计算的值只是threshold,而没有代码说明它是实际值,但本文最后的源码可以说明它就是实际值。


看到这个这个tableSizeFor方法,那么规则的1、2、4、8、16,是不是有点蒙。算法长成这样,是画画么?先了解下基础知识,一个整形由4个字节构成,那它转为二进制应该是32(4*8)位。但这是有符号的,其实正数和负数各占一半,也就是说各有2的31次方,最高位是符号位。我们这里是无符号操作就不用管这个符号了。上面的无符号右移16位就可以覆盖32位,16就是这么来的。其实这5个>>>(无符号右移)是为了把cap-1的最高二进制位及其后面的位都置成1。

最高二进制位是什么意思呢? 一个整形转为二进制后,最左边的那位一定是1,前面补的0不能算哈,这个最左的1就是最高二进制位。

按上面的算法,第一次无符号右移1位再和原二进制按位或,至少把最左边和次最左边的两个位置为1了吧。那第二次再只移一位就没意义了,因为前两位都是1了,不需计算了,所以第二次是右移2位,这样就有4个1了。同理4位变8位,8位变16位,最后16位变32位,实际是只有31位(最前的是符号位)。这样就把cap-1变为一个从其二进制形式的最高位到0位全为1的二进制数了,而这个二进制数正好是比cap-1大一点的那个2的n次方的数-1(如2的3次方减1就是1000-1=0111=7)。

最后一行再+1就是2的n次方了(7+1=8=2的3次方).而第一行的n=cap-1,这也就是防止传入参数就是2的n次的情况(如8的二进制是1000),再把这个最高二进制位的后面都置成1,最后再加1就变成16了。这就不是与传入参数最接近的2的n次方了。

这里也就回答了为什么map.length是2的n次方了,后面扩容的机制会证明它总是保持着2的n次方。


4.HashMap扩容机制是什么,什么时候扩,每次扩多少?

答:在put时,容量不够用的时候。因为每个元素都是一个单向链表,所以map里放的实际数量总是大于等于申请的空间。那么容量不够用就是实际存放的数量等于申请的空间大小的时候。看下源码put->putVal:


上面我们已经知道,HashMap的实际容量是2的n次方了。那么扩容也就是原来的容量*2,也就是扩到2的n+1次方。构造函数里有个加载因子(loadFactor),我曾经以为扩容就是扩这个数(默认0.75)+1倍,看到这里的源码才发现错了。这个加载因子只是计算下一个2的n次用的中间变量。默认的是0.75,那计算下一个2的n次方的基准值就是old*2*0.75,那么下一个2的n次方就是比这个基准值大的最少值(old*2)。如下图:


从上面代码可以看出,这个扩容代码是相当精妙的,更多有意思的地方,请同学自行debug。


最后感谢海生和我讨论这些问题,打赏的钱分他1/3。


<think>好的,我现在需要处理用户关于Java HashMap面试题及答案的查询。首先,用户提供了几个引用资料,我需要仔细阅读这些资料,提取关键信息,然后组织成结构清晰的回答。 首先,引用[1]提到HashMap是Java中重要且常用的数据结构,面试中常被问到,并列举了一些经典问题,比如工作原理。引用[2]和[3]都强调了HashMap在面试中的高频出现,通常会出现在前几题,考察候选人对JDK源码的理解。引用[4]详细列出了多个面试题,包括数据结构、哈希冲突处理、扩容机制等。引用[5]则给出了回答HashMap底层实现时的要点,如初始容量、负载因子、红黑树转换等。 接下来,我需要根据这些信息整理出常见的面试题及答案。要注意用户提到的系统级指令,比如行内数学公式用$...$,独立公式用$$...$$,但这里可能用不到数学公式。不过需要确保回答结构清晰,分点列出问题和答案,并正确添加引用标识。 可能的面试题包括: 1. HashMap的工作原理? 2. 如何处理哈希冲突? 3. 扩容机制是怎样的? 4. 为什么使用红黑树? 5. HashMap和HashTable的区别? 6. 初始容量和负载因子的作用? 7. 哈希函数如何实现? 8. 线程安全问题? 9. 与ConcurrentHashMap的区别? 10. 使用场景? 对于每个问题,需要结合引用中的内容给出简明扼要的回答,并在末尾添加引用标记。例如,工作原理涉及数组+链表+红黑树的结构,哈希冲突通过链表和红黑树解决,扩容机制涉及重新计算索引等。 需要确保答案准确,引用正确的来源,比如引用[4]提到jdk7和jdk8的结构变化,引用[5]提到初始容量和负载因子。同时,注意回答要符合中文表达,避免技术术语使用错误。 最后,生成相关问题,可能包括优化方法、性能分析、具体源码实现等,确保问题与主题相关,帮助用户进一步学习。</think>### Java HashMap 常见面试题及答案 #### 1. HashMap 的底层数据结构是什么? JDK 1.7 及之前使用**数组+链表**,JDK 1.8 引入**数组+链表+红黑树**。当链表长度超过阈值(默认8)时,链表转换为红黑树以提升查询效率[^4]。 #### 2. HashMap 如何处理哈希冲突? - 通过**链地址法**(链表)解决冲突,即哈希值相同的键值对存储在同一个数组位置的链表中。 - JDK 1.8 后,当链表长度超过阈值,链表转换为红黑树,将查询时间复杂度从$O(n)$降至$O(\log n)$。 #### 3. HashMap 的扩容机制是什么? - 默认初始容量为16,负载因子为0.75。当元素数量超过$容量 \times 负载因子$时,触发扩容: - 容量扩大为原来的2倍。 - 键值对重新计算索引并分配到新数组中[^5]。 #### 4. 为什么 JDK 1.8 引入红黑树? 链表过长会导致查询效率降低,红黑树通过平衡二叉树的特性,将最坏情况下的时间复杂度从$O(n)$优化为$O(\log n)$[^4]。 #### 5. HashMap 和 HashTable 的区别? | **HashMap** | **HashTable** | |---------------------------|----------------------------| | 线程不安全 | 线程安全(方法加 synchronized 锁) | | 允许键/值为 null | 不允许键/值为 null | | 默认初始容量16 | 默认初始容量11 | | JDK 1.2 引入 | JDK 1.0 引入 | [^4] #### 6. 初始容量和负载因子的作用? - **初始容量**:决定哈希数组的初始大小,减少扩容次数。 - **负载因子**:控制扩容阈值(容量 × 负载因子),权衡时间与空间效率(默认0.75)。 #### 7. 哈希函数如何实现? 键的哈希值通过`hashCode()`计算,再通过扰动函数(异或高16位与低16位)减少哈希冲突: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 扰动函数使哈希分布更均匀[^4]。 #### 8. HashMap 为什么线程不安全? 多线程环境下,扩容时可能导致链表形成环形结构(JDK 1.7),或数据覆盖问题(JDK 1.8)。 #### 9. HashMap 和 ConcurrentHashMap 的区别? - **HashMap**:线程不安全,效率高。 - **ConcurrentHashMap**: - JDK 1.7 使用分段锁(Segment)。 - JDK 1.8 改用 CAS + synchronized 锁单个桶(Node),并发性能更高[^4]。 #### 10. HashMap 的应用场景? - 高频键值查询(如缓存)。 - 无需保证线程安全的场景。 - 需要快速插入、删除、查找(时间复杂度接近$O(1)$)。 --- §§ 1. HashMap 在多线程下如何避免死循环? 2. 如何设计一个自定义对象作为 HashMap 的键? 3. ConcurrentHashMap 的 size() 方法如何实现? 4. 为什么负载因子默认值是0.75? 5. HashMap 的键为 null 时存储在哪个位置? [^1]: 引用1 [^2]: 引用2 [^3]: 引用3 : 引用4 : 引用5
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值