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 是一个链表.即数 ...
随机推荐
- 【Matlab】求解复合材料层合板刚度矩阵及柔度矩阵
1. matlab文件结构 2. main.m代码 clc clear; warning off; %% %铺层角度数组 angles=[0 90 0]; % ° %单层厚度 ply_thicknes ...
- python 二级 语言基本元素笔记-字符串
l='12345' 1.递增顺序: 正向递增从0开始 负向从[-1]开始,l[1]=2,l[-1]=5 2.切片操作:左开右闭,l[2:5]=3,4 3.导入库,引入 库名.函数名 input函数 输 ...
- 自动化-Yaml文件读取函数封装
1.文件布局 打开文件修改读取方式为w load函数加载文件 class ReadConfiYaml: def __init__(self,yaml_file): self.yaml_file=yam ...
- 【SfM】Colmap和openMVG尝试记录
数据集来源:http://www.maths.lth.se/matematiklth/personal/calle/dataset/dataset.html Colmap与openMVG关于SfM稀疏 ...
- Manus,没有秘密「注解版」
近来Manus走红,「争论」不断,我也在前文<Manus爆火,是硬核还是营销?>中阐述过自Manus发布后,行业讨论以及开源复刻的信息,以及我们如何结合蚂蚁图计算(TuGraph)技术,实 ...
- containerd 配置使用私有镜像仓库 harbor
前言 当要从非安全的镜像仓库中进行 Pull.Push 时,会遇到 x509: certificate signed by unknown authority 错误提示: 这是由于镜像仓库是可能是 ...
- Golang 入门 : Go语言的设计哲学
前言 设计哲学之于编程语言,就好比一个人的价值观之于这个人的行为. 因为如果你不认同一个人的价值观,那你其实很难与之持续交往下去,即所谓道不同不相为谋.类似的,如果你不认同一门编程语言的设计哲学,那么 ...
- 大模型提示词(Prompt)模板推荐
只有提示词写得好,与大模型的互动才能更高效.提示词不仅仅是与AI对话的起点,更是驱动模型产生高质量输出的关键因素.本文将介绍大模型提示词的概念.意义,并分享一些实用的提示词模板,帮助AI玩家更好地利用 ...
- Nginx 301永久性转移
我有个域名www.taadis.com, 想永久性转移到taadis.com. 前言 看到很多网友的做法是把taadis.com & www.taadis.com等多个域名放到一个server ...
- RuoYi-vue配置记录
如果这个项目能顺利运行,标志着Springboot+vue的前后端环境都配好了. 一.官方文档 若依官方文档:介绍 | RuoYi,在这个地方克隆/下载项目源代码https://gitee.com/y ...