HashMap底层原理分析(结合面试问题分析)

2023-10-27

1、 为什么HashMap底层数组的容量总是2的幂次方?
答:因为hashmap的底层在计算一个entry存放在数组中的索引值的时候,采用哈希值运算,如果经过哈希算法得到的一个哈希值h的后面的二进制表示为:0101 0101,此时的数组的长度length为2的4次方=16,在计算索引的最后一步就是int index = h & length-1;那么length的二进制表示为:0001 0000,length-1的二进制表示:0000 1111,后面全部为1,那么和h经过与操作之后实际上就是h的后面所表示的数字,范围0000-1111即0-15,所以就能保证我们计算出来的索引值不会越界。
在这里插入图片描述
2、 根据源代码可以知道,HashMap中的key是可以为null的。并且由于map的key是唯一性的,因此key为null的只有一个键值对,并且这个entry实体存放在数组中索引为0的位置。索引为0 的位置只有一个元素(这里主要和Hashtable做比较,因为该种结构是不允许键或者值为null)。
3、 为什么HashMap底层在生成哈希值的时候,在已经根据key生成哈希值的条件下,还需要进行右移运算和异或运算?
答:还是1中的例子,比如我们此时的数组长度为16,那么一个key经过哈希值运算之后,肯定是32位的,但是实际上,我们在计算索引的时候,只有哈希值的低位参与了计算,没有使用到高位的信息,导致很多不同的key经过运算得到的index都是相同的,从而造成数组中某个index的链表很长,检索变慢。当通过使用将高位右移运算,可使得高位的信息参与到index的计算过程中。使得索引更平衡。
4、 HashMap底层中,有个加载因子,其作用是什么?
答:这个加载因子决定了hashMap底层的table数组的扩容机制,例如:如果此时的table数组的长度为16,加载因子为0.75(底层源码默认),那么如果此时map中元素的个数(对应底层的一个size变量)大于了数组的长度乘以加载因子,此时数组会进行扩容。
5、 HashMap底层实现结构jdk7和jdk8的区别?
答:JDK1.7中,hashmap底层采用的是数组+链表的方式实现,里面的一个阈值就是加载因子,用于决定扩容的机制。而在jdk1.8中,底层采用的是数组+链表+红黑树的结构实现的,里面多了一个阈值叫做树化阈值 ,这个阈值默认值为8,也就是每个链表中的节点的个数大于等于8个的时候,就需要把这个链表树化成一个红黑树;除此之外,有一个树化阈值就有一个对应的反树化阈值(默认值为6) ,就是当我们将红黑树上的链表依次删除之后,如果这个树上的元素的个数小于等于6的时候,就会将红黑树链表提高添加效率但是查询效率也不会很低。
6、 分析一下,为什么jdk8中hashmap的树化阈值(8)和反树化阈值(6)不一样呢?
答:将两个阈值设置不一样是因为为了防止我们频繁在一个索引位置进行添加和删除元素,从而进行频繁的树化和反树化,例如:我们在index为6的位置添加一个元素之后,这个位置的元素个数变成9个,那么就需要进行树化,但是当我们在这个index删除一个之后,如果反树化阈值也是8,那么就需要进行反树化,这个过程是耗时的,因此将树化阈值设置的比反树化阈值大是有道理的。
7、 Jdk8中,hashmap底层为什么采用的是红黑树而不是其他的树形结构呢?
答:从分析hashmap的底层源码可以知道,hashmap在jdk7中采用的是数组+链表的形式,这样的方式对于键值对的添加很方便,但是对于数据的查询(获取)就需要依次从头结点开始往后遍历,查询效率低下,采用红黑树,可以平衡添加和查询两者之间的效率。
8、 注意:在jdk1.8中,只要一个index位置的元素的个数大于等于8就一定会进行树化吗?
答:不一定。在jdk8的源码中,进行树化之前需要进行一个阈值判断,就是判断当前这个table数组的长度是否小于MIN_TREEIFY_CAPACITY(这个表示树化的最小容量,默认值为64),只有当数组table的长度大于这个阈值的时候才会将数组中每个索引处元素个数大于8的链表转化成红黑树。因为如果数组的长度本身很短,例如16的时候,我们可以考虑数组扩容,这样的话可以增强map的散列性并且同样有机会将长度大于8的链表拆分短。

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();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

9、Jdk8中,table数组的初始化和数组的扩容都是采用的一个resize()方法进行实现的。在进行扩容的时候,我们考虑下面的情况,如果现在table的长度为4,并且在index1的位置有三个Node节点(jdk8中修改为节点,jdk7中叫做Entry),那么在进行数组的扩容的之后(扩容到8),index1位置处的元素应该在newTable的哪些索引处呢?
答:扩容之后节点存放的新索引和原来的索引是有一个规律的,要么就是原来的索引,要么就是原来的索引加上原来数组的长度。如果oldIndex =1,oldTabl.length = 4;那么newIndex=oldIndex=1||newIndex=oldIndex+oldTable.Length = 5。这个规律不是凭空来的。
我们举例来说明一下这个规律的由来:
假如:此时的table.length = 4;加载因子为0.75;map中元素的个数size=3,此时元素我们加入一个键值对(k,v)(考虑的是这个键值对对应的key不在map的key中),假设我们对k根据哈希值算法得到一个哈希值的二进制表示为:… 1001 0001(我们不考虑前面的那些);那么这个新的node应该存放的位置就是:index = hash & (table.length-1) = 1001 0001 & 0000 0011 = 0000 0001,存放在index==1的位置,当这个节点加入map之后,size++变成了4>table.lengthload_Factory = 3;所以需要进行扩容(jdk8中是先加入节点再进行扩容的),现在我们还是计算刚刚加入的那个节点的存放位置。
hash还是一样的1001 0001,此时的table.length = 2
4 = 8;新的index = hash & (table.length-1) = 1001 0001 & 0000 0111 = 0000 0001,还是原来的位置,但是如果刚刚k的hash=1001 0101呢?原来的index还是1,但是新的index = 1001 0101 & 0000 0111 = 0000 0101 = 5,变成了原来索引的值加上原来数组的长度。
10、 HashMap中resize()扩容方法原理分析


```java
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) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
//当原来的数组不为null,也就是这个resize的作用是扩容而不是初始化
    if (oldTab != null) {
//遍历底层的table数组,也就是每个桶的位置,每个桶中都是一个链表
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
//将桶中的头节点放入赋值给变量e,如果e不为null说明桶中有节点,否则就没有节点,进行下一个桶的判断
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;  //这一步其实没有多大必要,但是可以帮助jvm进行垃圾回收
//如果桶中只有一个节点,也就是链表只有一个节点,将这个节点的key的hash值与新的容量的长度-1进行与操作得到这个节点在新数组中的index,并且将新数组中index位置的值设置为这个节点
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
//如果这个节点是树节点,说明当前这个桶中的数据已经不再是链表了,而是一个红黑树,这里就进行数的分割再分配到新数组的各个桶中
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//否则就说明这个桶的链表中节点个数不为1,并且不是一个红黑树
                else { // preserve order
//定义四个节点变量,loHead用于表示低索引桶中链表的头节点;loTail表示低索引桶中俩表的尾部节点;hiHead表示高索引桶中链表的头结点;hiTail表示高索引桶中链表的尾部节点。(这里之所以有高低索引的区分是因为:在oldTable中索引为index桶的链表中的所有节点,在进行扩容之后,这些元素要么放在新数组对应的index桶中,要么就放在新数组index+oldCap对应的桶中,这个规律是因为我们采用2的幂次方作为数组的长度,并且采用index = hash & table.length-1作为index的计算公式)
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
//这里的if,else主要是判断当前这个节点放在新数组的低索引桶中还是高索引桶中
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
//这里将低索引对应的链表头放在新数组低索引桶中
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
//这里将高索引对应的链表头放在新数组高索引桶中
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

**11、	HashMap底层为什么采用红黑树不采用其他树?**
答:因为红黑树可以实现键值对的查找和插入两种操作之间的性能平衡,如果采用严格的avl(平衡二叉树)实现,每次进行键值对插入或者删除的时候,为了保持这棵树的平衡性,需要对树结构进行旋转的可能性大,导致插入或者删除键值对的效率较低,但是avl树的查询效率高,而红黑树是和平衡树类似的结构,但是其平衡的要求没有那么严格,因此为了平衡插入和查找的效率,采用红黑树实现。
**12、红黑树的基本特性有哪些?**
(1)树上的每一个节点要么是黑色要么是红色;
(2)红黑树的根节点一定为黑色;
(3)如果一个节点为红色,那么该节点的父节点和其子节点一定不能为红色;
(4)任何一个节点到该子树对应的所有叶子结点的可能路径上,黑色节点的个数一样。


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HashMap底层原理分析(结合面试问题分析) 的相关文章

  • Maven UTF-8编码问题

    当我使用两个不同的项目运行下面的代码时 我得到不同的输出 String myString T rk e Karakter Testi i String value new String myString getBytes UTF 8 Sys
  • 协助持续进行 Java 到 C# 转换的工具

    如今 许多项目都是用 Java 编写的 其中一些最终转换为 C 以合并到 NET 中 我想到的例子有 log4net nhibernate 和 db4o 包括 Sharpen db4o 的工具 在内 您是否见过和 或使用过任何使连续转换变得
  • 如何从内容处置中读取编码的文件名

    我得到的内容处置标头值如下 附件 文件名 UTF 8 album jpeg 如何从中提取文件名 album jpeg 在查看该值时 它具有编码格式值 使用Spring的内容配置 https docs spring io spring doc
  • 超立方体错误。非法的最小或最大规格

    尝试从这里运行示例代码http tess4j sourceforge net codesample html http tess4j sourceforge net codesample html我收到一条错误消息 Error Illega
  • 具有“繁忙”线程的 threadPoolExecutor 如何被终止?

    我的问题有点复杂 让我尝试彻底解释一下 但如果您需要更多详细信息 请随时询问我 我会添加它们 我最近 通过实验 了解到 如果线程连续工作 例如 while true 循环中的整数运算 则中断线程对其没有影响 话题继续进行 就像什么都没发生一
  • 在 IIS 中运行 Java Web 应用程序

    有人找到了在 IIS 中运行 Java Web 应用程序的方法吗 在我看来 编写一个将 Jetty 或自定义 servlet 容器与 IIS 集成的 ISAPI 插件 这个词正确吗 应该是完全可能的 这样做的好处是 许多优秀的高端 Java
  • 如何暂停程序直到按下按钮?

    我使用从 jframe 扩展的类 它有一个按钮 我在程序中使用它 我希望当在我的程序中运行 jframe 时我的整个程序暂停 直到我按下按钮 我该怎么做 in c getch 做这个 我想要一个这样的功能 通过睡眠暂停执行 http dow
  • 在 Java 中停止线程? [复制]

    这个问题在这里已经有答案了 我正在编写一段代码 该代码连接到服务器 使用该连接生成一堆线程并执行一堆 东西 在某些情况下 连接会失败 我需要停止一切并从头开始使用新对象 我想在对象之后进行清理 但在线程上调用 thread stop 但此方
  • Java GUI,根据actionListener更改面板

    我在两个不同的面板中添加了两个按钮 如果单击第一个按钮 则需要转到下一个面板 其中包含第二个按钮 但是当我单击第一个按钮时 该按钮没有被替换 Java GUI import java awt event ActionEvent import
  • 在 Android 上解析 RSS

    我有几个 RSS 源需要为我的应用程序进行解析 我按照这里的优秀教程进行操作 http w2davids wordpress com android rssatom feeds parsing with rome http w2davids
  • 没有字符串参数构造函数/工厂方法可以从字符串值 ('') 反序列化

    我在使用时遇到了 json 解析问题ObjectMapper类来自com fasterxml jackson databind包 我得到的错误是 com fasterxml jackson databind JsonMappingExcep
  • IntelliJ 对于 Java 项目使用的默认构建过程是什么?

    直接从 IntelliJ 中的 IDE 构建 Java 项目非常好 它速度很快 而且很有效 我无法找到任何有关 IntelliJ 如何进行这些默认构建的文档 我猜它使用Ant 我想做的是为下载我的项目的任何人自动化这个快速 轻松的构建过程
  • 如何更改使用 Google ReCaptcha 版本 2 时的错误消息?

    当为 Google ReCaptcha 版本 2 选择多张照片时 会显示以下错误消息 需要多个正确的解决方案 请解决更多 如何将错误消息更改为我网站上的自定义消息 这是图像 我认为不可能在服务器端 在谷歌 进行 这可以在客户端通过利用 js
  • spring-hibernate 花费更多时间的任何原因?

    目前 我正在春季和冬眠期间从事一个项目 我来到这里 获取记录并在 JSP 中显示这些记录需要更多时间 我在各处都保留了时间戳 以查看哪里花费了更多时间 Time HomeController start 2014 07 09 18 58 5
  • 如何修复 java.lang.ClassNotFoundException: org.springframework.boot.configurationprocessor.json.JSONException 错误?

    当我在生产环境中将 Spring Boot 服务作为 Windows 服务运行时 出现以下错误 服务exe的创建者是Jar2exe https www jar2exe com java lang reflect InvocationTarg
  • 清单合并失败:需要为 显式指定 android:exported

    我的清单文件有问题 错误消息 清单合并失败 android 需要为 明确指定导出 面向 Android 12 及更高版本的应用需要指定显式值android exported当相应的组件定义了意图过滤器时 有关详细信息 请参阅 https d
  • 访问 JAR 资源

    我有一个jar包含我想要分发的资源 主要是缓存 日志记录等配置 的文件 我对这些资源的相对路径有问题 所以我做了我在另一个 stackoverflow 问题中发现的问题 该问题说这是一种有效的方法 ClassInTheSamePackage
  • 使用用户名和密码登录 LinkedIn 失败

    LinkedIn使用oauth登录其api 服务器中无法登录api 我尝试使用http请求登录linkedin并获取oauth verifier 但我得到了这样的回应 很抱歉 出现了问题 你的申请 请确保您 启用cookie并重试 或点击此
  • 如何在两种不同模式、两种布局中设置方向?

    我有一个叫做Main XML我将方向设置为纵向AndroidManifest xml 我也为 Honeycomb 设计了这个布局并将其放置在layout xlarge mdpi文件夹 但我想使用Main XML in layout xlar
  • 如何在android中使用Room Persistence ORM工具实现created_at和updated_at列

    我该如何实施created at and updated at在Android中使用Room Persistence ORM工具的列 可以在创建或更新表中的行时自动更新时间戳 我研究了很多网站 但仍然没有找到任何可以处理的结果middlew

随机推荐

  • lua元表的相关知识

    setmetatable 和getmetatable local a 8 local b s local t 1 2 在Lua代码中 只能设置table的元表 若要设置其它类型的值得元表 则必须通过C代码来完成 对于字符串 标准的字符串程序
  • Linux中apt命令

    apt简介 Advanced Packaging Tool apt 是Linux下的一款安装包管理工具 最初只有 tar gz的打包文件 用户必须编译每个他想在GNU Linux上运行的软件 用户们普遍认为系统很有必要提供一种方法来管理这些
  • Github的创建及使用

    Github创建 注册账号 进入GitHub官网 https github com 步骤1 注册账号 username 不能使用下划线 并且短横线不能打头 中文也是不合法昵称 email 要填写合法邮箱 并且是未在GitHub注册过的邮箱
  • 什么是大小端?如何确定大小端?

    一 什么是大小端 对于一个由2个字节组成的16位整数 在内存中存储这两个字节有两种方法 一种是将低序字节存储在起始地址 这称为小端 little endian 字节序 另一种方法是将高序字节存储在起始地址 这称为大端 big endian
  • 基于微信小程序的游泳馆管理系统

    末尾获取源码 开发语言 Java Java开发工具 JDK1 8 后端框架 SSM 前端 Vue 数据库 MySQL5 7和Navicat管理工具结合 开发软件 IDEA Eclipse 小程序 微信开发者工具 是否Maven项目 是 目录
  • DHCP协议工作原理(分配IP地址的方式)

    DHCP工作在应用层 使用UDP协议工作 负责给局域网内的用户分配IP地址 分配IP地址的方式有三种 手动配置 自动配置 动态配置 手动配置是指管理员手动给客户端配置一个特定的IP地址 自动配置是指服务器为第一次链接的客户端分配一个永久地址
  • 数据挖掘实验第一次作业

    import random from matplotlib import pyplot class MTKL def init self n m self n n self m m def MC self n self n m self m
  • c语言中%s的用法

    转自 https www pinlue com article 2020 03 3100 5310073904413 html C语言是计算机软件领域非常经典的编程语言 unix linux等众多操作系统均是由C语言编写而成 而在硬件控制
  • Vulkan下多线程渲染设计

    1 Vulkan 视角下的多线程渲染 首先我们需要从vulkan api的顶层框架上来看一下 它在哪些地方可以让我们并行 Vulkan API的基本框架 Vulkan不同于Gles只有一个 不被API暴露出来的 单一链条的cmdbuffer
  • java设计模式——解释器模式(Interpreter Pattern)

    概述 解释器模式是一种使用频率相对较低但学习难度较大的设计模式 它用于描述如何使用面向对象语言构成一个简单的语言解释器 在某些情况下 为了更好地描述某一些特定类型的问题 我们可以创建一种新的语言 这种语言拥有自己的表达式和结构 即文法规则
  • mac安装python3.6

    1 查看本机默认安装环境 通过uname a 查看系统位数 x86 64代表64位 使用python命令查看系统默认版本 OSX默认安装2 7 10 系统很多lib都是基于python2 7 因此还是不要卸载 2 下载python3 6 h
  • [255]如何查找Linux服务器上JDK安装路径?

    成功远程到你要部署软件的Linux服务器上 这是第一步 查看JDK版本 java version 查看java执行路径 which java 查看JAVA HOME路径 echo JAVA HOME 插卡PATH内容 echo PATH 想
  • java实现电子发票中的发票税号等信息识别的几种可用方案

    先说一下背景 今天领导突然说需要做一个电子发票中发票税号的识别 于是乎就开始去调研看有哪些方案 最先想到的就是OCR文字识别 自己去画框训练模型去识别税号等相关信息 话不多说开整思路 思路一 百度AI平台去直接调用 思路二 自己基于模型训练
  • Ubuntu /Window下 X2Go 安装&连接&同步/上传文件夹:复制、粘贴、桌面共享

    Ubuntu Window下X2Go安装 连接 同步 上传文件夹 一次性成功 X2Go 的优点 双向粘贴板 安装 就可使用 文件夹共享 在某些 linux发行版上 由于依赖包问题 解决起来麻烦 在 错误新题提示 在服务器端 为隐藏文件 在客
  • C语言中,变量的按作用域角度分类的几种情况

    c语言中 变量按作用域角度分 分为局部变量和全局变量 1 局部变量是在一个函数内部或一个代码块中定义的变量 只能在被函数和代码块范围内有效 如 void test int b 20 b是一个局部变量 在test函数内有效 int main
  • OSPF实验报告

    一 划分网段 将192 168 1 0 24划分为 192 168 1 0 26 192 168 1 128 26 192 168 1 64 26 192 168 1 192 26 二 添加ip地址 将192 168 1 0 26划分为网络
  • 蓝桥杯 调手表【第九届】【决赛】【B组】

    比较简单的题 看到网上题解基本都是bfs解法 发个贪心解法记录一下 include
  • VSCode格式化XML

    VSCode格式化XML 文章目录 VSCode格式化XML 1 结果展示 2 插件 notepad 自带的xml插件不是很好用 显示xml树的插件经常失灵 尝试在VSCode上找到了一些插件 很好的解决了XML的格式化和XML节点显示的问
  • 【华为OD机试真题 Python】数组二叉树

    前言 本专栏将持续更新华为OD机试题目 并进行详细的分析与解答 包含完整的代码实现 希望可以帮助到正在努力的你 关于OD机试流程 面经 面试指导等 如有任何疑问 欢迎联系我 wechat steven moda email nansun09
  • HashMap底层原理分析(结合面试问题分析)

    1 为什么HashMap底层数组的容量总是2的幂次方 答 因为hashmap的底层在计算一个entry存放在数组中的索引值的时候 采用哈希值运算 如果经过哈希算法得到的一个哈希值h的后面的二进制表示为 0101 0101 此时的数组的长度l