如果想透彻理解什么是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的数据结构和源码分析的更多相关文章

  1. Java1.7 HashMap 实现原理和源码分析

    HashMap 源码分析是面试中常考的一项,下面一篇文章讲得很好,特地转载过来. 本文转自:https://www.cnblogs.com/chengxiao/p/6059914.html 参考博客: ...

  2. JDK1.8 HashMap中put源码分析

    一.存储结构      在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构.它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置.Hash ...

  3. Quartz学习--二 Hello Quartz! 和源码分析

    Quartz学习--二  Hello Quartz! 和源码分析 三.  Hello Quartz! 我会跟着 第一章 6.2 的图来 进行同步代码编写 简单入门示例: 创建一个新的java普通工程 ...

  4. 并发-HashMap和HashTable源码分析

    HashMap和HashTable源码分析 参考: https://blog.csdn.net/luanlouis/article/details/41576373 http://www.cnblog ...

  5. OpenMP For Construct dynamic 调度方式实现原理和源码分析

    OpenMP For Construct dynamic 调度方式实现原理和源码分析 前言 在本篇文章当中主要给大家介绍 OpenMp for construct 的实现原理,以及与他相关的动态库函数 ...

  6. OPENMP FOR CONSTRUCT GUIDED 调度方式实现原理和源码分析

    OPENMP FOR CONSTRUCT GUIDED 调度方式实现原理和源码分析 前言 在本篇文章当中主要给大家介绍在 OpenMP 当中 guided 调度方式的实现原理.这个调度方式其实和 dy ...

  7. 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 ...

  8. Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...

  9. Kubernetes Job Controller 原理和源码分析(一)

    概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...

  10. HashMap原理及源码分析

    HashMap 原理及源码分析 1. 存储结构 HashMap 内部是由 Node 类型的数组实现的.Node 包含着键值对,内部有四个字段,从 next 字段我们可以看出,Node 是一个链表.即数 ...

随机推荐

  1. ZLMediaKit: 快速入门

    目录 ZLMediaKit是什么 编译 安装依赖库 构建项目 问题处理 问题1: srtp 未找到, WebRTC 相关功能打开失败 问题2:依赖库问题 安装 配置和运行 问题处理 问题1: 无权限监 ...

  2. PHP将变量存储在数据库中,读取并执行变量的方法

    http://www.edbiji.com/doccenter/showdoc/4/nav/1214.html 例如将下边的字符串存储到数据库中您好,您的验证码是".$authcode.&q ...

  3. selenium 提示消息一闪而过,怎么定位

    F12 点击source 下的暂停按钮

  4. python xlrd 读取表格 单元格值被覆盖

    代码实现顺序: 按行读取 按列读取 满足if条件 单元格值赋值给字典 实现代码: datas = []# 定义一个空列表 for i in range (3,nrows): sheet_data={} ...

  5. 【MIPS】内存小端存储与MARS显示

    1.小端存储方式:按字节逆序 以4Byte一个字为单位,其内按字节逆序排列 概念上-数据真值 机器码-内存存储 Mars显示 (+0) (+1) (+2) (+3) 0x12345678 0x78 0 ...

  6. uniapp vue3 setup + 云开发开发个人小程序

    最近使用uniapp vue3 setup + 云开发开发了个人小程序,设计使用figma软件,看下成品截图吧(可以直接微信搜索[识光]小程序体验,或者最底部有码可以直接扫)

  7. MySQL 常用优化

    目录 单表优化 字段 索引 查询SQL 引擎 MyISAM InnoDB 系统调优参数 升级硬件 读写分离 缓存 表分区 分区的好处是: 分区的限制和缺点: 分区的类型: 分区适合的场景有: 垂直拆分 ...

  8. 多态的前提--java进阶day02

    1.多态的前提条件 第一点和第二点都很好理解,第三点父类引用指向子类对象是什么意思?以下图进行讲解 我们以前的写法,如下图,叫做子类引用指向子类 那父类引用呢?就是把左边换成父类Animal即可 因为 ...

  9. 一个检查左右括号是否配对的语法检查器(c语言)

    目录 一.题目如下 二.解题思路 三.代码实现 四.测试结果 一.题目如下 通过键盘输入一个包括 '(' 和 ')' 的字符串string ,判断字符串是否有效.要求设计算法实现检查字符串是否有效,有 ...

  10. REST API从木愣到够呆

    目前准备写一个API服务,遵循REST规范,因为自己也是第一次接触这玩意,所以就以自我的认知和理解过程来记录,留爪. ##在REST里什么叫资源? 拿数据表为例: 现在有三张表:(此表非彼婊) sch ...