面试阿里,HashMap 这一篇就够了

本文通过对话形式详细介绍了 HashMap 的底层数据结构、优化点及插入流程,包括“数组+链表+红黑树”的设计原因、链表转红黑树的阈值、扩容策略以及计算索引的逻辑。此外,还探讨了 HashMap 的重要属性、性能优化和并发安全问题,适合面试和学习使用。
摘要由CSDN通过智能技术生成

微信搜索【程序员囧辉】,关注这个坚持分享技术干货的程序员。

我的最新文章:面试官:如何进行 JVM 调优(附真实案例)

目录

前言

正文

二狗:天天听你憨逼吹牛,是时候让你知道什么叫残忍了。

二狗:先来点简单的,介绍下 HashMap 的底层数据结构吧。

二狗:为什么要改成“数组+链表+红黑树”?

二狗:那在什么时候用链表?什么时候用红黑树?

二狗:为什么链表转红黑树的阈值是8?

二狗:(呦呦呦,时间和空间上权衡的结果,还装起B来了)那为什么转回链表节点是用的6而不是复用8?

二狗:(小菜鸡,懂得还不少)那 HashMap 有哪些重要属性?分别用于做什么的?

二狗:threshold 除了用于存放扩容阈值还有其他作用吗?

二狗:HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

二狗:(你他娘的是在绕口令吧)你这个*@%¥#&的N次方是怎么算的?

二狗:卧槽,还彪英文,来来来,这代码你给我解释下。

二狗:(这叼毛讲的还凑合啊,连我都听懂了)你说 HashMap 的容量必须是 2 的 N 次方,这是为什么?

二狗:你说 HashMap 的默认初始容量是 16,为什么是16而不是其他的?

二狗:为什么是0.75而不是其他的?

二狗:为什么不是 0.74 或 0.76?

二狗:那我们换个问题问吧,HashMap 的插入流程是怎么样的?

二狗:图里刚开始有个计算 key 的 hash 值,是怎么设计的?

二狗:为什么要将 hashCode 的高16位参与运算?

二狗:扩容(resize)流程介绍下?

二狗:红黑树和链表都是通过 e.hash & oldCap == 0 来定位在新表的索引位置,这是为什么?

二狗:介绍一下死循环问题?

二狗:(尼玛,没听懂,尴尬了)那总结下 JDK 1.8 主要进行了哪些优化?

二狗:除了 HashMap,还用过哪些 Map,在使用时怎么选择?

二狗:(不妙,这个 B HashMap 懂得比我还多,得赶紧溜)到时间和女朋友吃饭了,我们之后再分胜负。

推荐阅读


前言

某日,囧辉和同事二狗决定就谁是“&#¥*大厦11楼11室(只有囧辉和二狗两人)HashMap 最强者”展开一番较量。画面过于血腥,成年人请在未成年人陪同下观看。

正文

二狗:天天听你憨逼吹牛,是时候让你知道什么叫残忍了。

囧辉:二狗子,这屎可以乱吃,这话不能乱说哦。

二狗:先来点简单的,介绍下 HashMap 的底层数据结构吧。

囧辉:我们现在用的都是 JDK 1.8,底层是由“数组+链表+红黑树”组成,如下图,而在 JDK 1.8 之前是由“数组+链表”组成。

二狗:为什么要改成“数组+链表+红黑树”?

囧辉:主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。

二狗:那在什么时候用链表?什么时候用红黑树?

囧辉:对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。

对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。

二狗:为什么链表转红黑树的阈值是8?

囧辉:我们平时在进行方案设计时,必须考虑的两个很重要的因素是:时间和空间。对于 HashMap 也是同样的道理,简单来说,阈值为8是在时间和空间上权衡的结果(这 B 我装定了)。

红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价作者觉得不值得。

理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为 0.00000006(跟大乐透一等奖差不多,中大乐透?不存在的),这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。

二狗:(呦呦呦,时间和空间上权衡的结果,还装起B来了)那为什么转回链表节点是用的6而不是复用8?

囧辉:如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。

二狗:(小菜鸡,懂得还不少)那 HashMap 有哪些重要属性?分别用于做什么的?

囧辉:除了用来存储我们的节点 table 数组外,HashMap 还有以下几个重要属性:1)size:HashMap 已经存储的节点个数;2)threshold:扩容阈值,当 HashMap 的个数达到该值,触发扩容。3)loadFactor:负载因子,扩容阈值 = 容量 * 负载因子。

二狗:threshold 除了用于存放扩容阈值还有其他作用吗?

囧辉:在我们新建 HashMap 对象时, threshold 还会被用来存初始化时的容量。HashMap 直到我们第一次插入节点时,才会对 table 进行初始化,避免不必要的空间浪费。

二狗:HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

囧辉:默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。

二狗:(你他娘的是在绕口令吧)你这个*@%¥#&的N次方是怎么算的?

囧辉:Talk is cheap. Show you the code。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

二狗:卧槽,还彪英文,来来来,这代码你给我解释下。

囧辉:我们先不看第一行“int n = cap - 1”,先看下面的5行计算。

|=(或等于):这个符号比较少见,但是“+=”应该都见过,看到这你应该明白了。例如:a |= b ,可以转成:a = a | b。

HashMapJava中的一种数据结构,它提供了一种键值对的映射关系。它基于哈希表实现,可以快速地插入、删除和查找元素。HashMap中的键是唯一的,而值可以重复。 HashMap的循环可以通过以下几种方式实现: 1. 使用迭代器循环:可以通过调用HashMap的`entrySet()`方法获取键值对的集合,然后使用迭代器遍历集合中的元素。示例代码如下: ```java HashMap<String, Integer> hashMap = new HashMap<>(); // 添加键值对 hashMap.put("A", 1); hashMap.put("B", 2); hashMap.put("C", 3); // 使用迭代器循环 Iterator<Map.Entry<String, Integer>> iterator = hashMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); String key = entry.getKey(); Integer value = entry.getValue(); // 处理键值对 System.out.println("Key: " + key + ", Value: " + value); } ``` 2. 使用for-each循环:可以直接使用for-each循环遍历HashMap中的键值对。示例代码如下: ```java HashMap<String, Integer> hashMap = new HashMap<>(); // 添加键值对 hashMap.put("A", 1); hashMap.put("B", 2); hashMap.put("C", 3); // 使用for-each循环 for (Map.Entry<String, Integer> entry : hashMap.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); // 处理键值对 System.out.println("Key: " + key + ", Value: " + value); } ``` 3. 只循环键或值:如果只需要循环HashMap中的键或值,可以使用`keySet()`方法获取键的集合或使用`values()`方法获取值的集合,然后进行循环遍历。示例代码如下: ```java HashMap<String, Integer> hashMap = new HashMap<>(); // 添加键值对 hashMap.put("A", 1); hashMap.put("B", 2); hashMap.put("C", 3); // 循环键 for (String key : hashMap.keySet()) { // 处理键 System.out.println("Key: " + key); } // 循环值 for (Integer value : hashMap.values()) { // 处理值 System.out.println("Value: " + value); } ```
评论 159
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员囧辉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值