HashMap源码面试题

总体特点:

  1. java1.8之前使用的是数组+单向链表的数据结构,java1.8及之后使用的是数组+单向链表+红黑树的数据结构,在hash冲突形成的单向链表元素个数达到8个时会转成红黑树。
  2. 使用无参构造器初始化的node数组容量是16(在resize()方法中初始化),加载因子是0.75(也就是说当node数组中元素超过0.75容量时会进行扩容),扩容时新容量是旧容量的2倍。
  3. 扩容时java1.8之前使用的头插法,头插法可能会导致元素的丢失、hash环等问题,java1.8及之后使用的是尾插法,能够避免之前版本扩容时存在的问题
  4. HashMap中的元素不保证有序性,跟元素插入顺序会不一致
    实战经验及使用场景:
  5. HashMap作为key-value形式最常使用的集合,是一个线程不安全的集合,一般情况不建议使用在类成员变量中,如果要用作类成员变量,那么正确使用方式是用static代码块或构造器执行put操作,要不就是在put时使用lock或sycsynchronized使得put元素过程是单线程操作。
  6. 在初始化HashMap时最好初始化大小,防止频繁扩容
  7. hashmap如果做为类成员变量储存数据,则要考虑key清除策略,要不可能会导致内存泄漏
  8. HashMap中的元素不保证有序性,跟元素插入顺序会不一致,如果碰到需要加密或签名等场景则需要使用linkedHashMap
    一些显示技术能力及有思考的面试题:
    1.为什么用(h = key.hashCode()) ^ (h >>> 16)算hash?
    答:降低hash冲突的概率。比如key=”张三“。
    key.hashCode() 为:
    1111 1111 1111 1111 1111 1010 0111 1100
    此时的node数组大小为16,那么(n - 1) & hash会是下面的操作:
    1111 1111 1111 1111 1111 1010 0111 1100
    0000 0000 0000 0000 0000 0000 0000 0111
    那么实际上”张三“这个key的hashcode只有后三位会影响算数组的位置,其他位都是没影响的。
    那么如果使用(h = key.hashCode()) ^ (h >>> 16)呢?
    1111 1111 1111 1111 1111 1010 0111 1100
    0000 0000 0000 0000 1111 1111 1111 1111
    1111 1111 1111 1111 0000 0101 1000 0011
    那么最后进行(n - 1) & hash操作的hashcode就变了,使得之前没影响的高16位的hash也参与了数组位置的决定,这就会舍得hashcode冲突的概率降低。
    (n - 1) & hash为什么能保证数组下标不能越界呢?
    答:这就跟hashMap容量有关,hashMap巧妙的利用了2的幂方来作为容量,默认情况下初始容量为16,之后扩容是旧容量的2倍。我们以16为例:16-1 =15,写成二进制:1111,这种二进制跟任何一个二进制取&都会小于15,这就相当于%操作,但&的效率会高很多。下标现在是可以确定了,但如果元素的hash值不够散列,就会造成hash碰撞问题,hash值相同的key会以单链表或红黑树的形式储存。Hash碰撞比较严重的话会严重影响hashMap查询和插入的性能,所以应该尽量使hash值随机。
    长度为什么是的2的幂次方
    答:1. 可以使用(n - 1) & hash方式确定下标,效率更高
  9. 扩容时重新计算位置会更加简单(针对有hash冲突情况),扩容后新容量是旧容量的2倍,相当于原二进制向左移动一位,比如之前容量是16,二进制是1 0000,扩容后是32,二进制是
    10 0000.也就是元素的高位部分最后一个会参与(n - 1) & hash位置计算,而使用hash&oldCap方式就能确定参与运算的高位会不会改变原来的位置.当参与计算的高位是1时,新位置=j+oldCap,当参与计算的高位是0时,新位置=j。这里涉及的代码逻辑在resize()方法中:

1.8如何解决了1.7之前会造成死循环的问题?
答:

  1. 1.7会造成死循环的由来
    比如现在hashmap中的table是这样的

其中key:张三,李四,王五hash冲突了形成了一个单链表,正常根据上面的代码会重新定位node在新数组中的位置在放到新数组中,此时我们再假设这三个key在新数组中还是hash冲突了,此时数组就变成了:

这就是所谓的头插法,在单线程扩容的看起来一点问题没有,但多个线程进行扩容就不行了,比如现在有线程a和线程b来扩容
线程a先在正常执行上诉扩容的逻辑,并且扩容到了如下步骤:

此时线程b发现也需要扩容,他也开始进行扩容执行上诉逻辑,那么他开始的时候:
e="李四“
next="张三”
table[i]="李四“
然后执行e.next = newTable[i]就变成"李四“.next = ‘’"李四“
newTable[i] =‘’"李四“;

经过线程b的完整扩容之后就变成了这样,当执行get王五时就死循环了。
2. 1.8如何解决死循环
从上面可以看出,形成环的主要原因是要把单链表倒置,也就是头插入,如果是尾插入就不会改变next指针的指向。也就不会形成环状链表,所以1.8使用了尾插入,而且使用尾插入还有一个好处是hash冲突元素扩容新位置会很好计算,上面已阐释,这里就不展开说明了。
1.8 还有什么线程安全问题?
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null)
可能会形成数据覆盖,线程a和线程b同时执行(p = tab[i = (n - 1) & hash]) == null)且i是一样的,之后线程a挂起,线程b执行,之后线程a再执行就会覆盖线程b的元素。
++size
可能使size跟实际size不一样。

欢迎关注公众号:java面试工程师

<think>好的,我现在需要处理用户关于Java HashMap面试题及答案的查询。首先,用户提供了几个引用资料,我需要仔细阅读这些资料,提取关键信息,然后组织成结构清晰的回答。 首先,引用[1]提到HashMapJava中重要且常用的数据结构,面试中常被问到,并列举了一些经典问题,比如工作原理。引用[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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值