Java进阶(HashMap)——面试时HashMap常见问题解读 结合源码分析

news2024/9/25 21:21:40

在这里插入图片描述

前言

List、Set、HashMap作为Java中常用的集合,需要深入认识其原理和特性。

本篇博客介绍常见的关于Java中HashMap集合的面试问题,结合源码分析题目背后的知识点。

关于List的博客文章如下:

  • Java进阶(List)——面试时List常见问题解读 & 结合源码分析

关于的Set的博客文章如下:

  • Java进阶(Set)——面试时Set常见问题解读 & 结合源码分析

其他关于HaseMap的文章如下:

  • Java学数据结构(3)——树Tree & B树 & 红黑树 & Java标准库中的集合Set与映射Map & 使用多个映射Map的案例
  • Java学数据结构(4)——散列表Hash table & 散列函数 & 哈希冲突

目录

  • 前言
  • 引出
  • HashMap底层结构是什么?
    • JDK 1.7和1.8 对比
    • 源码Node类
  • HashMap如何解决Hash碰撞问题?HashMap 何时从单链表,转换为红黑树?
    • 当链表节点的数量达到8个时,通过treeify转为红黑树
  • HashMap的扩容机制?HashMap的数组何时需要扩容?
    • 1.首次添加元素
    • 2.初始容量16
    • 3.大于16时,双倍扩容
    • 总结分析
  • HashMap设置长度为11,那么数组的容量为多少?
    • 第一个2的幂次方的值
  • HashMap 何时从红黑树转换为 单链模式?
    • split方法和untreeify方法
  • HashMap 为什么在多线程并发使用过程中,容易造成死循环/死锁?
  • 如何得到一个线程安全的HashMap集合?
  • 总结

引出


1.jdk1.7 HashMap:数组+单向链表;
2.jdk1.8 HashMap:数组+链表(单向)+红黑树;
3.当链表节点的数量达到8个时,通过treeify转为红黑树;
4.首次添加元素,初始容量16,大于16时,双倍扩容;
5.HashMap设置长度,第一个2的幂次方的值;
6.红黑树元素的高位或者低位节点个数<6时,那么就调用untreeify方法来退回链表结构;
7.jdk1.7采用的是头插法,即新来元素在链表起始的位置,而jdk1.8采用尾插法,可以有效的避免在多线程操作中产生死循环;
8.ConcurrentHashMap高并发线程安全;

核心:键值对,KEY不可重复,VALUE可以重复

HashMap底层结构是什么?

JDK 1.7和1.8 对比

jdk1.7 HashMap:数组+单向链表

jdk1.8 HashMap:数组+链表(单向)+红黑树

源码Node类

源码可以看到HashMap内部定义了静态Node类,Node类中成员有

在这里插入图片描述

Node<K,V> next;

同样可以看到HashMap内部定义了静态TreeNode类,TreeNode类中成员有

在这里插入图片描述

TreeNode<K,V> left;
TreeNode<K,V> right;

可以看出存在红黑树

而TreeNode继承了 LinkedHashMap.Entry,点进查看,可以看到 Entry也继承了 HashMap.Node。所以,TreeNode红黑树是从链表Node中转换过来的

在这里插入图片描述

整体图:

在这里插入图片描述

HashMap如何解决Hash碰撞问题?HashMap 何时从单链表,转换为红黑树?

  1. 首先理解什么是hash碰撞问题,HashMap存放的元素的KEY需要经过hash计算得到hash值,而这个hash值可以理解为就是此元素要存放的位置,即数组中的下标;但是如果两个不同元素经过hash计算后,得到的hash值相同时,即两个元素要存放的位置为同一个位置,产生冲突,这种现象就叫做hash碰撞。
  2. 要想了解HashMap是如何解决hash碰撞的,那我们就需要看看HashMap的put方法的源码中的核心操作

在这里插入图片描述

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
     //添加第一个元素时,会进入这个if结构,table为null,则第一次初始化这个table数组的长度为16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
     //判断添加的元素的KEY经过hash计算得到的下标位置是否为null
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果是null,则直接添加元素
            tab[i] = newNode(hash, key, value, null);
     //不为null的情况
        else {
            Node<K,V> e; K k;
            //如果key相同,则用新的元素添加到旧元素的位置,后续会将就元素返回
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判断是否为红黑树节点,如果是红黑树节点,则添加为红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //链表类型
            else {
                //通过for循环遍历链表节点
                for (int binCount = 0; ; ++binCount) {
                    //如果链表节点next节点为空
                    if ((e = p.next) == null) {
                        //则添加至链表的next节点属性中
                        p.next = newNode(hash, key, value, null);
                        //如果链表节点 >= 7 说明链表存在8个已存的元素节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //转为红黑树方法
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果KEY相同,匹配其他API 如 putIfAbsent()
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //存入新值,返回旧值
             if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
 ....

当链表节点的数量达到8个时,通过treeify转为红黑树

总结:HashMap是通过3层 if 结构来判断,数组下标位置是否有元素和下标位置的类型是链表还是红黑树然后通过链表和红黑树来解决hash碰撞的问题,当链表节点>=7时(当链表节点的数量达到8个时),会通过treeify转为红黑树

HashMap的扩容机制?HashMap的数组何时需要扩容?

1.首次添加元素

HashMap在第一次添加元素时,会进入第一个if结构来初始化数组的长度

在这里插入图片描述

2.初始容量16

//添加第一个元素时,会进入这个if结构,table为null,则第一次初始化这个table数组的长度为16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

在这里插入图片描述

3.大于16时,双倍扩容

此处resize方法就是扩容方法,jdk8中,resize方法除了扩容还增加了初始化的功能,进入此方法我们可以看一下源码

在这里插入图片描述

 final Node<K,V>[] resize() {
     Node<K,V>[] oldTab = table;
     int oldCap = (oldTab == null) ? 0 : oldTab.length;
     int oldThr = threshold;
     int newCap, newThr = 0;
     if (oldCap > 0) {
         //如果当前数组的长度>=最大值(2^30)时,将预值threshold设置为最大值
         if (oldCap >= MAXIMUM_CAPACITY) {
             threshold = Integer.MAX_VALUE;
             return oldTab;
         }
         //如果当前数组的长度>=默认的初始长度16,则双倍扩容
         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
             newThr = oldThr << 1; // double threshold
     }
     ...

调用resize方法的地方

在这里插入图片描述

      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {    
          ...
              ++modCount;
          if (++size > threshold)
              resize();
          afterNodeInsertion(evict);
          return null;

总结分析

  1. 从上面可以看出,HashMap在完成put元素存储后,会判断++size是否>了阈值,如果是就会去扩容
  2. 下面这个方法是在,put元素为链表节点,并且要转为红黑树时,会调用该方法,该方法会在一开始就判断是否需要扩容

在这里插入图片描述

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
  1. 判断扩容的核心就是threshold这个值

在这里插入图片描述

  1. 从resize方法中看到,HashMap在扩容时,是之前的双倍扩容

HashMap设置长度为11,那么数组的容量为多少?

第一个2的幂次方的值

指定了长度初始化HashMap时,它会将数组的容量经过一系列算法,设置为大于我们自定义值的第一个2的幂次方的值,

即 设置为11 , 则为2^4=16

在这里插入图片描述

在这里插入图片描述

 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;
    }

HashMap 何时从红黑树转换为 单链模式?

在这里插入图片描述

  1. HashMap在resize方法执行时,会将元素从旧数组转入新数组,此时如果转移元素为红黑树结构,那么就会调用split方法来分割红黑树方便转移
  2. split方法内部,在分割时,会生成高位树与低位树两种此时也会进行判断如果红黑树元素的高位或者低位节点个数<6时,那么就调用untreeify方法来退回链表结构

split方法和untreeify方法

在这里插入图片描述

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    ...        //lowhead   低位树
        if (loHead != null) {
            //在红黑树节点元素往新数组中添加时,会调用split方法来重组这个红黑树
            //此时会判断,红黑树的节点操作次数是否<6,即low树(低位树的节点数)< 6时,会通过untreeify方法来退化为链表
            if (lc <= UNTREEIFY_THRESHOLD)
                tab[index] = loHead.untreeify(map);
            else {
                tab[index] = loHead;
                if (hiHead != null) // (else is already treeified)
                    loHead.treeify(tab);
            }
        }
        //此时会判断,红黑树的节点操作次数是否<6,即high树(高位树的节点数)< 6时,会通过untreeify方法来退化为链表
        //highhead  高位树 
        if (hiHead != null) {
            if (hc <= UNTREEIFY_THRESHOLD)
                tab[index + bit] = hiHead.untreeify(map);
            else {
                tab[index + bit] = hiHead;
                if (loHead != null)
                    hiHead.treeify(tab);
            }
        }
}

在这里插入图片描述

HashMap 为什么在多线程并发使用过程中,容易造成死循环/死锁?

图示:

在这里插入图片描述

  1. 产生死循环的核心原因是因为,jdk1.7采用的是头插法,即新来元素在链表起始的位置,而jdk1.8采用尾插法,可以有效的避免在多线程操作中产生以上死循环
  2. 但是HashMap不是线程安全的,所以在多线程的场景中,虽然不会出现死锁/死循环,但是还是会出现节点丢失的情况,所以在并发的场景中推荐使用ConcurrentHashMap

如何得到一个线程安全的HashMap集合?

在这里插入图片描述

ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap<>();

总结

1.jdk1.7 HashMap:数组+单向链表;
2.jdk1.8 HashMap:数组+链表(单向)+红黑树;
3.当链表节点的数量达到8个时,通过treeify转为红黑树;
4.首次添加元素,初始容量16,大于16时,双倍扩容;
5.HashMap设置长度,第一个2的幂次方的值;
6.红黑树元素的高位或者低位节点个数<6时,那么就调用untreeify方法来退回链表结构;
7.jdk1.7采用的是头插法,即新来元素在链表起始的位置,而jdk1.8采用尾插法,可以有效的避免在多线程操作中产生死循环;
8.ConcurrentHashMap高并发线程安全;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1151699.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CPU架构之x86解读

一&#xff0e;什么是x86架构 X86架构&#xff1a;是微处理器执行的计算机语言指令集&#xff0c;指一个intel通用计算机系列的标准编号缩写&#xff0c;也标识一套通用的计算机指令集。 编辑搜图 二、x86架构的优势 技术成熟&#xff1a;x86架构的芯片经过多年的发展&#…

Linux:KVM虚拟化

本章操作基于centos7系统 简介 KVM是Kernel Virtual Machine的简写&#xff0c;目前Redhat只支持在64位的Rhel5.4以上的系统运行KVM&#xff0c;同时硬件需要支持VT技术。KVM的前身是QEMU&#xff0c;在2008年被redhat公司收购并获得了一项hypervisor技术&#xff0c;不过redh…

UE学习记录08----Actor 绕指定Actor旋转

Sphere 以 Cube为中心点 在水平面 即 xy平面进行旋转 来源&#xff1a; UE4 actor绕着某个actor旋转 - 简书 (jianshu.com)

【EI会议征稿】第三届绿色能源与电力系统国际学术会议(ICGEPS 2024)

第三届绿色能源与电力系统国际学术会议&#xff08;ICGEPS 2024&#xff09; 2024 3rd International Conference on Green Energy and Power Systems 绿色能源是指可以直接用于生产和生活的能源。它包括核能和“可再生能源”。随着世界各国能源需求的不断增长和环境保护意识…

启动Vue项目报错Error: error:0308010C:digital envelope routines::unsupported

问题描述 启动Vue项目报错Error: error:0308010C:digital envelope routines::unsupported 出现这个一般就是node版本的问题&#xff0c;通过命令查看node -v查看node版本&#xff1b; 百度查了好多&#xff0c;都让我降低node版本&#xff0c;属实太麻烦了 在不改node版本的…

WhatsApp是什么?如何使用?有何功能?

相信很多做跨境的小伙伴对WhatsApp并不陌生&#xff0c;现在它是数十亿人使用的最受欢迎的即时通讯平台&#xff0c;每天在该平台上交换超过100亿条消息。它在全球200个国家地区拥有超过20亿用户&#xff0c;在60个国家属于常用通讯设备&#xff0c;是世界上最受欢迎的消息传递…

【广州华锐互动】飞机诊断AR远程指导系统为工程师提供更多支持

随着科技的发展&#xff0c;飞机的维护工作也在不断进步。其中&#xff0c;AR&#xff08;增强现实&#xff09;技术的应用使得远程运维成为可能。本文将探讨AR在飞机诊断远程指导系统中的应用&#xff0c;以及它对未来航空维护模式的影响。 AR远程指导系统是一种使用增强现实技…

对接第三方接口鉴权

我们知道&#xff0c;做为一个web系统&#xff0c;少不了要调用别的系统的接口或者是提供接口供别的系统调用。从接口的使用范围也可以分为对内和对外两种&#xff0c;对内的接口主要限于一些我们内部系统的调用&#xff0c;多是通过内网进行调用&#xff0c;往往不用考虑太复杂…

虹科干货 | CAN与CAN FD总线常见故障诊断及解决

全文导读&#xff1a;CAN总线凭借着可靠、实时、经济和灵活的优势&#xff0c;在汽车、工业等领域得到广泛应用&#xff0c;并逐渐普及到电池储能、医疗器械、智能大楼等应用场景中。随着CAN总线在越来越多领域得到应用&#xff0c;CAN总线测试的需求也逐渐增多。本文主要总结了…

c++之内联函数

要学习内联函数&#xff08;inline&#xff09;&#xff0c;首先我们要复习一下宏的缺点 我们的目的就是用内联函数去替代宏。 内联函数类似与宏的优点&#xff0c;但是克服了宏的缺点。&#xff08;内联函数编译器通过宏实现了内联函数&#xff09; 但是一般只适合小函数&…

车载网关产品解析(附:车载网关详细应用案例及部署流程)

5G车载网关是一款功能强大的工业级无线通讯设备。它集成了4G/5G双模网络模块、M12接口设计、强大的路由和安全功能等特性,可以为车载和移动应用提供稳定可靠的无线数据连接。 链接直达&#xff1a;https://www.key-iot.com/iotlist/sv900.html ### 产品特性 5G车载网关最大的…

一百九十八、Java——IDEA项目中有参构造、无参构造等快捷键(持续梳理中)

一、目的 由于IDEA项目中有很多快捷键&#xff0c;可以很好的提高开发效率&#xff0c;因此整理一下 二、快捷键 &#xff08;一&#xff09;快捷键生成public static void main(String[] args) {} 快捷键&#xff1a;psvm &#xff08;二&#xff09;快捷键在test中创建cn…

一文6个步骤带你实现接口测试入门

一、接口测试概述 1 什么是接口测试&#xff1a; 接口测试是测试系统组件间交互的一种测试。接口测试主要用于检测外部系统与系统之间&#xff0c;内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互逻辑…

在线客服源码系统+完全开源可二开 带完整搭建教程

在线客服源码系统是一种基于互联网技术的自动化客户服务软件。它通过网页、即时通讯等多种方式&#xff0c;实现企业与客户之间的即时互动和信息交流。该系统具有多种功能&#xff0c;如在线聊天、文件传输、消息管理、用户信息存储等。选择合适的在线客服源码系统&#xff0c;…

Spring两大核心之一:AOP(面向切面编程)含设计模式讲解,通知类型切点;附有案例,实现spring事务管理

模拟转账业务 pom.xml <dependencies><!--spring--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.29</version></dependency><!--lombok-->…

Jmeter 测试 MQ 接口怎么做?跟我学秒变大神!

MQ(message queue)消息队列&#xff0c;是基础数据结构 先进先出 的一种典型数据结构。一般用来解决应用解耦&#xff0c;异步消息&#xff0c;流量削锋等问题&#xff0c;实现高性能&#xff0c;高可用&#xff0c;可伸缩和最终一致性架构。 MQ 主要产品包括&#xff1a;Rabb…

ChatGLM3 模型学习与实战

文章目录 一&#xff0c;说明二、前言1. 背景2. 系统说明3. 相比于ChatGLM2-6B进行的 性能升级4. 模型列表 三、环境搭建1. 下载代码2 构建环境3 安装依赖4. 大模型下载方式4.1 安装 lfs 方便本地下载 ChatGLM2-6B 大模型4.2 Hmodelscop 上手动下载模型及其文件 【推荐】4.3 Hu…

大中小企业自招人力及劳务派遣全行业招聘抖音直播招聘效果佳

在抖音平台上&#xff0c;企业或者人力资源公司可以通过直播的形式&#xff0c;将职位以视频直播的方式展现出来。通过抖音直播招聘报白&#xff0c;企业或者人力资源公司可以利用抖音的短视频流量红利&#xff0c;触达到每天超过8亿的活跃用户群体。这样可以提高岗位信息的曝光…

python爬取百度图片上的图像

from fake_useragent import UserAgent import requests import re headers {"User-agent": UserAgent().random, # 随机生成一个代理请求"Accept-Language": "zh-CN,zh;q0.9,en;q0.8,en-GB;q0.7,en-US;q0.6","Connection": "k…

大厂面试题-JVM如何判断一个对象可以被回收

在JVM里面&#xff0c;要判断一个对象是否可以被回收&#xff0c;最重要的是判断这个对象是否还在被使用&#xff0c;只有没被使用的对象才能回收。 1. (如图)引用计数器&#xff0c;也就是为每一个对象添加一个引用计数器&#xff0c;用来统计指向当前对象的引用次数&…