前两天面试的时候,被面试官问到HashMap底层原理,之前只会用,底层实现完全没看过,这两天补了补功课,写篇文章记录一下,好记性不如烂笔头啊,毕竟这年头脑子它记不住东西了哈哈哈。好了,言归正传,今天我们就来深入探索下HashMap 。

哈希表作为一种优秀的数据结构,本质上存储结构是一个数组,加以链表和红黑树辅助。JDK 1.7 之前没有红黑树,仅仅是链表作为辅助;在JDK 1.8 以后新增了红黑树,效率得到大大优化,至于怎么优化的,我们后续再进行分析。我们知道数组结构在查询和插入删除的时间复杂度分别为O(1)和O(n),链表结构在查询和插入删除的时间复杂度分别为O(n)和O(1),二叉树在这两者之间做了平衡,查询和插入删除的时间复杂度都为O(logn),而在哈希表中两者均为O(1),由此可见哈希表的优越性。

首先来看一下HashMap的结构

可以看到,HashMap是由一个数组构成的,数组中存的是链表或者红黑树(后面再进行分析)。

下面我们来介绍几个常量

// 默认初始化容量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30;

// 负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 转化为红黑树的长度 static final int TREEIFY_THRESHOLD = 8;

// 退化为链表的长度 static final int UNTREEIFY_THRESHOLD = 6;

首先,我们先来创建一个哈希表,它的键是字符串,值对应一个学生对象

我们首先来看new一个哈希表时都进行了哪些操作,这就得看它的构造函数了 ,在JDK源码中,有四种构造函数,分别如下

可以看到,我们刚刚在创建HashMap的时候使用的是第3种构造方法,在这个方法中很简单,就是初始化了一个默认的负载因子,默认值为0.75。我们看在第1种构造方法初始化了一个容量,但是刚刚我们创建的时候没有指定容量,那么它怎么往其中添加元素呢?不急,我们接着往下看

当我们调用put方法时,内部调用了putVal方法 ,其中key就是哈希表的键,即我们传进去的“学生一“,value就是学生对象。 这里调用了hash()方法,用来计算键值key的哈希值

相同的key一定会得到相同的哈希值,不同的key也有可能得到相同的哈希值,这就是所谓的哈希碰撞,那么发生哈希碰撞了 怎么办?这个我们后面再说。先看一下putVal方法做了哪些工作

在这里,我们看到一个Node[]数组,它就是我们前面所说的,里面存的是链表。在往哈希表中put元素的时候,首先会进行上图中的第一步,检查该数组是否为空,为空的话则要执行resize()方法,进行扩容。我们刚刚在创建HashMap的时候,没有指定初始容量,仅仅初始化了一个负载因子,所以说此时数组是空的,在put第一个元素的时候自然要执行resize()方法。这个方法我们后面再讲。在初始化了数组之后进行put操作,首先会根据key的哈希值寻找该元素在数组中应该放置的位置,也就是上图中标号为2的代码。这里解释一下该段代码

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

其中i = (n-1) & hash得到的是该元素放置位置的索引值 ,这里的n是数组的长度,由于在计算机中是使用&来进行按位取余的,采用的都是二进制数,所以无论key的哈希值是多少,i的值一定会被限制在n-1之间,即在数组的合法下标之中。比如我们假设:n=16,元素的哈希值为hash=41,那么在计算机中表示为n-1 = 0000 1111 ,hash = 0010 1001 ,这里我们省略了高位,仅表示到第8位,那么在执行(n-1) & hash之后,i = 0000 1001 ,也就是十进制数字9。

在得到下标之后,判断该位置是否已经有了元素,没有的话则放到该位置。如果该位置已经存在元素,说明此时发生了哈希碰撞,则进入碰撞处理。碰撞处理的思路如下:下面单独将该部分代码贴出来进行分析

首先进行1处的判断,这里的判断是什么意思呢?该部分其实做的是值更新的处理,即如果我们的哈希表中已经存在一个键为key1的数据,当我们再插入一个键为key1的数据时, 新的值替换旧值,键还是原来的key1。这个时候也能说明相同的键会有相同的哈希值,自然要进行哈希碰撞处理了。

代码2部分比较简单,进行红黑树的碰撞处理,那么什么时链表会转化为红黑树呢? 前面有两个常量

// 转化为红黑树的长度 static final int TREEIFY_THRESHOLD = 8;

// 退化为链表的长度 static final int UNTREEIFY_THRESHOLD = 6;

其中TREEIFY_THRESHOLD = 8表示呢,当链表节点数量达到8时,则将链表转化为红黑树以提升效率。既然可以转过去,当然也可以转回来,当我们删除链表中的节点到6个的时候,红黑树退化成为链表。

如果1和2部分都不满足的话,那么就说明是在链表中产生了哈希碰撞。代码3就是处理链表中的哈希碰撞的,需要注意的是,在此处处理时需要判断是否需要将链表转化为红黑树。

put操作说完了 ,接下来看下是如何从HashMap中取出一个元素的,get方法用来获取一个元素,如下所示

在该方法中调用了getNode方法用来返回一个节点,源码中getNode方法如下

该方法相对来说就简单多了,我们还是将其分为三个部分。首先根据哈希值计算出其在数组中的索引值,如果存在,则根据具体情况进入代码1、2、3。

每次都是先判断索引处的第一个元素,无论该处是链表还是红黑树,符合情况就返回并结束。这也是代码1的意思。如果不是,则根据该处是链表还是红黑树,分别进行相应的查找办法,对应上面的代码2、3。

最后,来简单介绍一下扩容机制,那么什么时候会调用resize进行扩容呢?上面我们提到了“负载因子”这个概念,这个时候它就派上用场了。当我们哈希表的存储的元素个数超过DEFAULT_LOAD_FACTOR * Cap时(也就是达到当前容量的 0.75 倍)就需要进行扩容了 ,这是一个相当费时的操作。下面我们来看一下,是如何进行扩容的

上面这部分就是一些边界判断,真正的精华看下面这部分

主要上图圈红的部分,每次扩容新表容量变为旧表的2倍。对旧表中的每个位置进行遍历,如果该位置只有一个元素,则放到原位置不动。该位置若为树则进入树的分解方法(这里我们暂且不讨论)。下面分析一下链表的分解方法:

由于数组扩容为原来的2倍,就把原来的单链表拆分为2队(采用hash & OldCap分解),一队奇队,一队偶队。然后分别放在原位置j和新位置j + OldCap,至此,扩容完成。

好了,到这里HashMap的基本原理已经介绍完了,内容属实不少,脑阔疼。最后我们再来看几个小问题:Java 强调在重写equals()方法时,必须重写hashCode方法,这是为什么呢?因为在两个键的hash值相等的时候,会去调用equals()方法判断两者是否为同一对象,默认的equals()方法在比较时是比较两个变量的内存地址,此时一定是不相等的。假设我们有两个相等的键,key1和key2(但是在内存中的位置不同), 在重写euqals()方法之前,key1.equals(key2) = False,重写之后 ,key1.equals(key2) = True,在 Java 中希望把它们当做同样的键来处理。下面我们用一个例子来进行说明:

这里我们没有重写hashCode()和equals()方法,可以看到map中的两个key是一样的,但是此时输出的map.size = 2。

重写了这两个方法之后,输出的map.size = 1,可以看到此时map中才是不存在相同的键的。

2. 负载因子是大点好,还是小点好?由于数组是定长的,当添加的元素增多时,发生哈希碰撞的概率就增大,此时哈希表会逐渐退化成链表。Java 中默认的负载因子为0.75,较大的负载因子会导致哈希碰撞增多,较小的负载因子则会导致内存浪费。

2020.08.31 更

前天面试依图科技时,HashMap又被再一次拿了出来。在这次面试中,面试官又提及了几个问题,是之前没有注意到的,特此来巩固一下。先来回顾一下问题:initialCapacity指的是什么?

HashMap在什么时候进行扩容?

好!首先来看第一个问题,我们都知道当HashMap的容量(存储的key-value的节点数量)达到最大容量的DEFAULT_LOAD_FACTOR(0.75)时,就需要进行扩容了。那么问题来了,这个最大容量指的是数组的长度还是HashMap中key-value的个数呢?在源码中,Cap这个量一直是与数组长度和threshold相关的。

上面是resize()方法中的一段,无论我们在初始化的时候指定initialCapacity是多少,都不会立即给它分配内存。当在put第一个元素的时候,首先会进行扩容,此时的扩容是跟构造函数息息相关的。如果初始化的时候没有指定initialCapacity,那么加载默认的负载因子。此时threshold = 0

initialCapacity = 0

此时在resize()方法中进行第一次扩容的时候,直接进入情况C,使用默认的容量个负载因子构建HashMap如果初始化的时候制定了initialCapacity,即便仅仅指定了initialCapacity一个参数,也会调用HashMap(int initialCapacity, float loadFactor)函数,在这个方法中,有一个关键函数this.threshold = tableSizeFor(initialCapacity);

tableSizeFor()函数用于指定threshold。截止到此时,只指定了threshold的值,Cap仍然为0。在第一次put的时候,进入情况BnewCap=OldThr除了这两种情况之外,其他情况直接进入A(在第一次put之后,需要扩容的情况),newCap=OldCap << 1

newThr=OldThr << 1

容量和阈值都扩为原来的两倍。

之后就是使用更新好的newCap创建新的Node[]数组。

下面看第二个问题:什么时候进行扩容?扩容和Cap这个量没有直接关系,Cap代表的是Node[]数组的容量,根据Cap和loadFactor会计算出一个threshold。在每次put元素时,会更新size变量,每次递增1,size代表的是HashMap中的Node的数量,当size>threshold时,调用resize()方法进行扩容。

需要牢记一点即可:resize和Cap没有直接关系,和threshold有直接关系,但是threshold又与Cap有关threshold第一次确定与Cap有关

之后threshold的值都更新为原来的2倍

下面贴一个HashMap容量初始化的博客java中hashmap容量的初始化 - 杨冠标 - 博客园​www.cnblogs.com

作者:孙旭森

联系方式:2909300605

注:转载请注明出处!

java map原理_Java HashMap底层原理分析相关推荐

  1. java hashtable 数据结构_java Hashtable底层原理是怎样的?数据结构包括什么?

    大家都知道,随着近些年科学技术水平的不断进步与发展,学习编程语言的人也越来越多了.很多人对于java中的一些常见知识点有些不了解,今天就来为大家详细介绍一下. 首先说一下具体的概念: HashTabl ...

  2. java map操作_Java HashMap的基本操作

    Java HashMap的基本操作 import java.util.Collection; import java.util.HashMap; import java.util.Map.Entry; ...

  3. HashMap底层原理分析(put、get方法)

    1.HashMap底层原理分析(put.get方法) HashMap底层是通过数组加链表的结构来实现的.HashMap通过计算key的hashCode来计算hash值,只要hashCode一样,那ha ...

  4. HashMap底层原理(当你put,get时内部会发生什么呢?)

    HashMap底层原理解析(一) 接触过HashMap的小伙伴都会经常使用put和get这些方法,那接下来就对HashMap的内部存储进行详解.(以初学者的角度进行分析)-(小白篇) 当程序试图将多个 ...

  5. 深度解剖HashMap底层原理

    HashMap底层原理 写在前面 JDK1.7版本--HashMap java.1.7源码分析 new一个HashMap实例的存储流程图如下: API常用方法 API中重要的变量 第一步:申明一个Ha ...

  6. 我向面试官讲解了hashmap底层原理,他对我竖起了大拇指

    前言: 正值金九银十的黄金招聘期,大家都准备好了吗?HashMap是程序员面试必问的一个知识点,其内部的基本实现原理是每一位面试者都应该掌握的,只有真正地掌握了 HashMap的内部实现原理,面对面试 ...

  7. 聊聊Java系列-集合之HashMap底层结构原理

    前言           由于HashMap在我们的工作和面试中会经常遇到,所以搞懂HashMap的底层结构原理就显得十分有必要了.在JDK1.8之前,HashMap的底层采用的数据结构是数组+链表, ...

  8. 【Java集合】一文快速了解HashMap底层原理

    目录 一.HashMap底层的数据结构(简单讲解原理) 1.1 当我们向HashMap存入一个元素的时候 1.2 当我们取获取这个元素的时候 二.JDK 1.8中对hash算法和寻址算法是如何优化的? ...

  9. Java集合—HashMap底层原理

    原文链接:最通俗易懂搞定HashMap的底层原理 HashMap的底层原理面试必考题.为什么面试官如此青睐这道题?HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好o ...

最新文章

  1. 笔记本电脑摄像头实现光流跟踪
  2. 实现定时中断_无线传感器网络实验报告(二)Timer定时应用实验
  3. 纯干货:Linux抓包命令集锦(tcpdump)
  4. [有限元]利用虚位移和虚力的定义、对称性推导弹性力学公式
  5. R语言从原点开始作图
  6. 重磅分享(二)——决策引擎实战部署
  7. 重磅开源!《阿里巴巴Android开发手册》抢鲜下载!
  8. Google App Engine 功能被滥用于创建无限制的钓鱼页面
  9. Tomcat6配置参数详解
  10. 怎么制作自己的压缩软件
  11. 营销养号、封号、解封方法_微信公众号
  12. 爱企查爬虫selenium
  13. Mac的日常使用之免费NTFS for Mac (mounty)一款免费的NTFS 。畅快的使用移动硬盘
  14. Java随机26位英文字母
  15. oracle实例由,Oracle 数据库的实例由( )组成
  16. 注册验证码短信收不到是怎么回事
  17. tansig、logsig公式与导数推导
  18. 图片转素描的工具汇总
  19. android 多层json,Android json解析:根据嵌套key值逐层获取最底层数据
  20. python判断字符串包含某个字符_python判断字符串是否包含另一个字符串

热门文章

  1. python订单管理系统软件_有什么订单管理软件系统是好用的?
  2. 如何在ASP.NET Core中编写自定义日志记录提供程序
  3. Angular 7和ASP.NET Core 2.1入门
  4. 有效的数据处理:使用Tango库进行压缩和加密
  5. c# -- 二维码生成
  6. stat在python中_stat模块接口
  7. 引入react文件报错_react.js引入router文件后报错
  8. 计算机一级考试第一套题电子表格,计算机等级考试一级上机试题(第一套)
  9. c语言 链表首部插入数据,在链表中插入数据!求助!!!
  10. mybatis-plus实现自定义字段修改数据 后续更新CRUD