news 2026/8/21 18:47:34

美团面试题:Hashmap的结构,1.7和1.8有哪些区别,深入的分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
美团面试题:Hashmap的结构,1.7和1.8有哪些区别,深入的分析

(一) 真实面试题之:Hashmap的结构,1.7和1.8有哪些区别

不同点:
(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

(2)扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)

2、而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。

在计算hash值的时候,JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。

扩容流程对比图:

(3)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)

这里在重新进行补充两个问题:(2019-09-03)

(1)为什么在JDK1.7的时候是先进行扩容后进行插入,而在JDK1.8的时候则是先插入后进行扩容的呢?

其实就是当这个Map中实际插入的键值对的值的大小如果大于这个默认的阈值的时候(初始是16*0.75=12)的时候才会触发容,

//这个是在JDK1.8中的先插入后扩容 if (++size > threshold) resize();

其实这个问题也是JDK8对HashMap中,主要是因为对链表转为红黑树进行的优化,因为你插入这个节点的时候有可能是普通链表节点,也有可能是红黑树节点,但是为什么1.8之后HashMap变为先插入后扩容的原因,我也有点不是很理解?欢迎来讨论这个问题?
但是在JDK1.7中的话,是先进行扩容后进行插入的,就是当你发现你插入的桶是不是为空,如果不为空说明存在值就发生了hash冲突,那么就必须得扩容,但是如果不发生Hash冲突的话,说明当前桶是空的(后面并没有挂有链表),那就等到下一次发生Hash冲突的时候在进行扩容,但是当如果以后都没有发生hash冲突产生,那么就不会进行扩容了,减少了一次无用扩容,也减少了内存的使用

void addEntry(int hash, K key, V value, int bucketIndex) { //这里当钱数组如果大于等于12(假如)阈值的话,并且当前的数组的Entry数组还不能为空的时候就扩容 if ((size >= threshold) && (null != table[bucketIndex])) { //扩容数组,比较耗时 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //把新加的放在原先在的前面,原先的是e,现在的是new,next指向e table[bucketIndex] = new Entry<>(hash, key, value, e);//假设现在是new size++; }

(2)为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢(面试蘑菇街问过)?

如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
还有一点重要的就是由于treenodes的大小大约是常规节点的两倍,因此我们仅在容器包含足够的节点以保证使用时才使用它们,当它们变得太小(由于移除或调整大小)时,它们会被转换回普通的node节点,容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值

//Java中解释的原因 * Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million



(二)哈希表如何解决Hash冲突?


(三)为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化


(四)为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键


(五)HashMap 中的 key若 Object类型, 则需实现哪些方法?

(六)总结与面试要点

1. JDK 1.7 与 JDK 1.8 HashMap 核心区别总结

下表总结了 JDK 1.7 与 JDK 1.8 中 HashMap 的主要差异:

对比维度JDK 1.7JDK 1.8
数据结构数组 + 单向链表数组 + 单向链表 + 红黑树
插入方式头插法(新节点插入链表头部)尾插法(新节点插入链表尾部)
扩容时机先扩容,后插入先插入,后扩容
hash 计算9次扰动(4次位运算 + 5次异或)2次扰动(1次位运算 + 1次异或)
扩容后位置计算重新计算 hash & (newLength-1)利用规律:原位置 或 原位置+旧容量
树化阈值无树化机制链表长度 ≥ 8 且桶容量 ≥ 64 时树化
退化阈值无退化机制树节点数 ≤ 6 时退化为链表
并发问题头插法可能导致环形链表死循环尾插法减少死循环风险,但仍非线程安全

2. 高频面试追问方向

方向一:为什么 HashMap 线程不安全?

  • JDK 1.7:多线程扩容时,头插法可能导致环形链表,造成死循环。
  • JDK 1.8:虽然改为尾插法避免了环形链表,但 put/get 操作未加锁,多线程同时修改仍会导致数据覆盖、size 计算不准确等问题。
  • 追问点:能否举例说明并发 put 如何导致数据丢失?ConcurrentHashMap 如何解决这些问题?

方向二:负载因子为什么默认是 0.75?

  • 空间与时间平衡:0.75 是统计学上的一个折中值。负载因子过小(如 0.5)会导致频繁扩容,空间利用率低;负载因子过大(如 0.9)会导致哈希冲突增加,链表变长,查询效率下降。
  • 数学依据:根据泊松分布,当负载因子为 0.75 时,哈希冲突的概率相对较低,同时空间利用率较高。
  • 追问点:如果已知数据量固定,如何设置初始容量和负载因子以优化性能?

方向三:为什么树化阈值是 8,退化阈值是 6?

  • 树化阈值 8:基于泊松分布,链表长度达到 8 的概率极低(约 0.00000006)。超过 8 时,红黑树的 O(log n) 性能优势开始显现,且 TreeNode 大小约为 Node 的两倍,仅在必要时才转换。
  • 退化阈值 6:设置一个缓冲区间(7),避免频繁的树-链表转换。如果在 8 附近频繁插入删除,阈值 6 和 8 的差值可以防止反复转换带来的性能开销。
  • 追问点:为什么不用 AVL 树而用红黑树?红黑树在 HashMap 中的优势是什么?

3. 面试实战建议

  1. 结合源码回答:能说出关键方法的名称和大致逻辑(如 resize()、putVal()、treeifyBin())。
  2. 对比记忆:将 1.7 和 1.8 的区别归纳成表格,便于清晰表达。
  3. 延伸思考:除了上述区别,还可以准备「为什么 HashMap 容量总是 2 的幂次方?」、「hash 方法的具体实现?」、「多线程下如何安全使用 HashMap?」等延伸问题。

掌握这些核心区别和追问方向,不仅能应对基础面试题,还能在深度追问中展现对 HashMap 设计哲学的深入理解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/21 18:43:58

用DevExpress实现基于HTMLCSS的桌面应用程序的UI(二)

DevExpress WinForm拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForm能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜任…

作者头像 李华
网站建设 2026/8/21 18:38:45

【工作记录】F12导入接口信息至postman/apifox/jmeter

解决问题&#xff1a;无法快速获取接口参数信息 1. 复制为CURL 2. import导入postman&#xff0c;点击send请求&#xff0c;请求成功 2. import导入apifox&#xff0c;点击send请求&#xff0c;请求成功 3、import导入jmeter jmeter接口导入方式_jmeter导入文件接口-CSDN博客文…

作者头像 李华