如果想透彻理解什么是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. Abaqus-Steady-State-Dynamic-Analysis的求解原理

    0. 总括 基于模态的谐响应分析,可以通过扫频的方式求解频率范围内结构的线性稳态响应情况.阻尼是和频率相关的,但模态叠加法只需要知道n个模态阻尼即可推广到其他频率范围(原因详见文内公式). 1. 谐响 ...

  2. AWVS安装使用

    AWVS安装使用 1.双击exe文件,然后点击下一步. 2.选择我接受,然后下一步. 3.选择路径(我选择的默认路径)然后下一步. 4.还是下一步. 5.设置邮箱,用户名密码,用户名12345678@ ...

  3. hexo 本地启动项目 hexo-browsersync 不工作原因总结

    问题 1 : hexo-server 开启 compress 压缩后 hexo-browsersync 插件热更新完全无效,没办法自动刷新 此问题表现在 hexo 任何版本 问题原因: hexo-se ...

  4. linux clickhouse 密码设置

    默认密码 clickhouse 安装好之后,系统默认的登录账号密码是 /etc/clickhouse-server/users.d/default-password.xml 文件中配置的,默认密码是 ...

  5. 原生JS实现虚拟列表(不使用Vue,React等前端框架)

    好家伙,   1. 什么是虚拟列表 虚拟列表(Virtual List)是一种优化长列表渲染性能的技术.当我们需要展示成千上万条数据时,如果一次性将所有数据渲染到DOM中,会导致页面卡顿甚至崩溃.虚拟 ...

  6. Pydantic字段级校验:解锁@validator的12种应用

    title: Pydantic字段级校验:解锁@validator的12种应用 date: 2025/3/23 updated: 2025/3/23 author: cmdragon excerpt: ...

  7. Delphi 中拖动无边框窗口的5种方法

    1.MouseMove事件中加入: // ReleaseCapture; // Perform(WM_SYSCOMMAND, $F017 , 0); 2.MouseDown事件中加入: // POST ...

  8. MaxKB+Ollama 离线部署

    主题:在 Centos7 环境部署 MaxKB 以及 Ollama 实现基于离线大模型的的小助手调用. 选择离线部署的原因:原计划是打算直接使用 1Panel 进行 MaxKB 和 Ollama 一键 ...

  9. Hello, PostgreSQL

    Hello, PostgreSQL Whoami:5年+金融.政府.医疗领域工作经验的DBA Certificate:PGCM.OCP.YCP Skill:Oracle.Mysql.PostgreSQ ...

  10. ApplicationContext 接口的实现类

    ClassPathXmlApplicationContext: 它是从类的根路径下加载配置文件 推荐使用这种 FileSystemXmlApplicationContext: 它是从磁盘路径上加载配置 ...