HashMap的数据结构和源码分析
如果想透彻理解什么是HashMap,首先需要知道HashMap的数据结构是什么;其次需要厘清它能做什么,即它的功能;最后,还需要知道HashMap怎么实现这些功能的。下面我们针对这三个方面展开剖析。
什么是HashMap
在疯狂飙车之前,先聊聊 HashMap 是什么吧!HashMap是基于哈希表设计的、Map接口的非同步(synchronized)实现。此实现提供所有可选的映射操作,并允许使用null值和null键。它存储的是键值对,速度很快。它不保证映射的顺序,特别是不保证该顺序恒久不变。
HashMap实际上是一个“链表散列”的数据结构,关于其底层实现,在Java7中依靠数组+单链表实现,自Java8开始依靠数组+单链表+红黑树实现。它平衡了多种数据结构的优缺点,实现了寻址容易,插入删除也容易。
HashMap有哪些构造函数?它共包括4个构造函数:
- public HashMap()// 默认构造函数,常用
- public HashMap(int initialCapacity, float loadFactor) / /指定容量大小和加载因子
- public HashMap(int initialCapacity) // 指定容量大小,常用
- public HashMap(Map<? extends K, ? extends V> m) // 包含子Map,将m中的全部元素逐个添加到HashMap中
Java 7中HashMap的数据结构
在Java 7中,它的数据结构示意图如下:

数组中的每个元素都是Entry<K,V>类型的一个对象。当出现哈希冲突的时候,把发生冲突的元素放入相同散列地址中,构造成一个单链表。Entry<K,V>定义如下所示:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next; // ①
int hash; // ②
/** * Creates new entry. */
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
在代码①,next存储指向下一个Entry的引用。代码②是对key的hashCode值进行哈希运算后得到的数组索引,存储在Entry中以避免重复计算。
简单来说,HashMap由数组+单链表组成的,左侧的数组是HashMap的主体,也称为哈希数组,数组的每个元素都是一个单链表的头节点;右侧的单链表则是主要为了解决哈希冲突而存在的,如果根据哈希函数定位到的哈希地址不含链表(当前entry的next指向null),那么对于查找和添加等操作速度很快,仅仅需要一次寻址即可。如果定位到的数组包含链表,则对于添加操作,其时间复杂度为O(n),需要首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对判断是否相等。所以,考虑到时间复杂度,在HashMap中链表出现的越少,性能就会越好。
Java 8中HashMap的数据结构
在Java 8中,HashMap的数据结构是数组+单链表+红黑树。之所以引入红黑树,是因为遍历单链表的时间复杂度是O(n),而红黑树的是O(logn)。它的数据结构示意图如下:

从JDK8开始,HashMap将插入的键值对封装在Node对象中,每个Node对象包含四个属性——hash值,键对象key,值对象value和指向下一个元素的next。Node是HashMap的一个内部类,实现了Map.Entry接口,本质上就是一个映射(键值对)。Node对象定义如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
红黑树中,节点类型TreeNode定义如下:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
红黑树比链表多了四个变量,parent父节点、left左节点、right右节点、prev上一个同级节点,红黑树内容较多,不在赘述。当遇到哈希碰撞时,新增的Node会被next变量指向,组成单链表。当该单链表的长度超过8时,将单链表转换为红黑树。而内部类TreeNode是Node的子类,关系图如下:

下图同样很形象的展示了HashMap的数据结构(数组+链表+红黑树),桶(bucket)中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。

有了数组+单链表+红黑树3个数据结构,我们可以大致联想到HashMap的实现了。首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
扩容时如何确定数组大小
在介绍如何确定数组索引之前,先介绍三个本文即将用到的位运算符:
1. >>> : 无符号右移,忽略符号位,空位都以0补齐;
2. ^ : 按位异或运算,第一个操作数的第n位于第二个操作数的第n位相反,那么结果的第n为也为1,否则为0;
3. & : 按位与运算,针对二进制,只要有一个为0,就为0。
下面分析求table大小的方法:
/**
*计算大于给定容量的最小的2的幂次,扩容后的容量必须是2的幂次
*/
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
//从二进制cap的最左边的1开始,全部设置为 1,得到 n,这样 n + 1就是要求的值
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); // cap - 1 再计算避免cap假设刚好是8,但 n=16 这是不对的
// cap 是 0 或 1 的时候 n 是 -1,此时返回 1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这就保证了HashMap扩容后,容量总是2的幂次。
确定元素数组索引位置
不管增加、删除或者查找键值对,定位元素存储的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀,尽量使得每个位置上的元素数量只有一个,那么当我们用哈希函数求得这个位置的时候,马上就可以知道数组索引所对应的元素就是我们想要的,不用遍历链表,大大优化了查询的效率。先看看源码的实现:
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//①
}
//Java7的源码,Java8没有这个方法,但是实现原理一样
static int indexFor(int h, int length) {
return h & (length-1); //取模运算
}
函数hash(Object key)中,代码①的实现可以拆分为如下两步:
第一步计算hashCode值:h = key.hashCode();
第二步应用位运算: h ^ (h >>> 16)。
从Java 8开始,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么设计可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与散列地址的计算中,同时不会有太大的开销。
函数hash(Object key)在Java 7和Java 8之后的版本中都有。至于indexFor函数,则是计算数组索引的最后一步取模运算:h & (length-1)。
Java 7中封装了indexFor函数,但在Java 8中不再单独抽象为一个方法,但是采用了同样的计算原理。所以最终存储位置的确定流程是这样的:

上述两个函数共同构成了HashMap中计算元素散列地址的哈希函数。这个函数被定义的非常巧妙,它通过h & (table.length -1)来得到给定元素的哈希地址,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
Reference
https://www.jianshu.com/p/aa715ff9a572
http://www.importnew.com/20386.html
https://www.cnblogs.com/xiaoxi/p/7233201.html
HashMap的数据结构和源码分析的更多相关文章
- Java1.7 HashMap 实现原理和源码分析
HashMap 源码分析是面试中常考的一项,下面一篇文章讲得很好,特地转载过来. 本文转自:https://www.cnblogs.com/chengxiao/p/6059914.html 参考博客: ...
- JDK1.8 HashMap中put源码分析
一.存储结构 在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构.它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置.Hash ...
- Quartz学习--二 Hello Quartz! 和源码分析
Quartz学习--二 Hello Quartz! 和源码分析 三. Hello Quartz! 我会跟着 第一章 6.2 的图来 进行同步代码编写 简单入门示例: 创建一个新的java普通工程 ...
- 并发-HashMap和HashTable源码分析
HashMap和HashTable源码分析 参考: https://blog.csdn.net/luanlouis/article/details/41576373 http://www.cnblog ...
- OpenMP For Construct dynamic 调度方式实现原理和源码分析
OpenMP For Construct dynamic 调度方式实现原理和源码分析 前言 在本篇文章当中主要给大家介绍 OpenMp for construct 的实现原理,以及与他相关的动态库函数 ...
- OPENMP FOR CONSTRUCT GUIDED 调度方式实现原理和源码分析
OPENMP FOR CONSTRUCT GUIDED 调度方式实现原理和源码分析 前言 在本篇文章当中主要给大家介绍在 OpenMP 当中 guided 调度方式的实现原理.这个调度方式其实和 dy ...
- Android Debuggerd 简要介绍和源码分析(转载)
转载: http://dylangao.com/2014/05/16/android-debuggerd-%E7%AE%80%E8%A6%81%E4%BB%8B%E7%BB%8D%E5%92%8C%E ...
- Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析
相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...
- Kubernetes Job Controller 原理和源码分析(一)
概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...
- HashMap原理及源码分析
HashMap 原理及源码分析 1. 存储结构 HashMap 内部是由 Node 类型的数组实现的.Node 包含着键值对,内部有四个字段,从 next 字段我们可以看出,Node 是一个链表.即数 ...
随机推荐
- [解决方案]git pull : error: cannot lock ref 'refs/remotes/origin/*' (unable to update local ref)
错误 git pull 报错不能更新本地分支 错误分析 本地分支跟远程分支不匹配 导致更新失败 解决方案 备份自己修改的代码 .git\refs\remotes (文件路径)对应删除你报错的分支 gi ...
- python实现监控站点目录,记录每天更新内容,并写入操作日志,以便查找病毒恶意修改
问题描述:站点需要追溯代码的修改时间,以便尽早发现病毒恶意修改迹象,及时处理 运行环境:linux服务器,宝塔面板 示例代码:一.读取txt的文件路径,依次遍历所有目录下面的文件,并记录文件信息 pa ...
- vue2 配置 mock.js 模拟后端数据
安装 mockj 首先确保你有一个 vue 2 项目,如果没有,可以用 Vue CLI 创建一个: vue create vue-mock-demo 开始安装 Mock.js npm install ...
- MySQL REPLACE INTO语句
介绍 在向表中插入数据时,我们经常会:首先判断数据是否存在:如果不存在,则插入:如果存在,则更新. 但在 MySQL 中有更简单的方法,replace into(insert into 的增强版),当 ...
- crontab使用路径的问题
crontab工具的一个大问题就是不能支持相对路径,会导致文件不能找到,在crontab启用脚本中加入cd指令,使得工作目录切换到运行工具所需的目录,即可 * 定时任务 每天凌晨0点执行 * 00 0 ...
- 分布式任务调度系统 xxl-job
微服务难不难,不难!无非就是一个消费方,一个生产方,一个注册中心,然后就是实现一些微服务,其实微服务的难点在于治理,给你一堆 微服务,如何来管理?这就有很多方面了,比如容器化,服务间通信,服务上下线发 ...
- SQL语句(一)—— DDL
SQL 全称 Structured Query Language,结构化查询语言.操作关系型数据库的编程语言,定义了一套操作关系型数据库统一标准 . 一.SQL 基础知识 (一)SQL 通用语法 在学 ...
- STM8S003驱动TM1650偶发性故障
故障现象:STM8S003驱动TM1650数码管显示,偶发TM1650无法初始化造成数码管点不亮. 已经在程序中对TM1650初始化之前加上了延时,但是问题并未改善. 之前发生过类似情况,STM8S0 ...
- 在我用了几个月VSCode的C++及其衍生功能后的感受
VSCode优点槽点大盘点 优点 衍生功能是真的多,几乎所有功能在扩展市场里面都能得到.而且无需配置环境啊 自定义功能是真的方便,可以自定义.exe存放位置或者名称,打下;键就能够一键美化代码等等 美 ...
- 关闭windows10 Alt+Tab开打edge选项卡
发现最近更新的windows10会使用快捷键Alt+Tab打开Edge的选项卡,很不适应,可喜的是微软提供了关闭的方法. 设置⚙->系统->多任务处理->Alt+Tab 设置为仅打开 ...