Java HashMap底层实现

2023-11-13

HashMap 是 Java 使用频率最高的用于映射(键值对)处理的数据类型。JDK1.8 对 HashMap 底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。在JDK1.8以前HashMap是由数组+链表的数据结构组成的。

Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:

下面针对各个实现类的特点做一些说明:

(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。


 存储结构

从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。

(1) 从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。我们来看Node[JDK1.8]是何物。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    //用来定位数组索引位置
        final K key;
        V value;
        Node<K,V> next;   //链表的下一个node

        Node(int hash, K key, V value, Node<K,V> next) { ... }
        public final K getKey(){ ... }
        public final V getValue() { ... }
        public final String toString() { ... }
        public final int hashCode() { ... }
        public final V setValue(V newValue) { ... }
        public final boolean equals(Object o) { ... }
}

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象。

(2) HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。例如程序执行下面代码:

    map.put("名字","小铭");

系统将调用"美团"这个key的hashCode()方法得到其hashCode 值(该方法适用于每个Java对象),然后再通过Hash算法的后两步运算(高位运算和取模运算,下文有介绍)来定位该键值对的存储位置,有时两个key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结果越分散均匀,Hash碰撞的概率就越小,map的存取效率就会越高。

如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。那么通过什么方式来控制map使得Hash碰撞的概率又小,哈希桶数组(Node[] table)占用空间又少呢?答案就是好的Hash算法和扩容机制。

在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码可知,构造函数就是对下面几个字段进行初始化,源码如下

     int threshold;             // 所能容纳的key-value对极限 
     final float loadFactor;    // 负载因子
     int modCount;  
     int size;

首先,Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor = 16*0.75 = 12。也就是说初始化时数组长度为1在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳最大键值对数量threshold的区别。而modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化

 在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,具体证明可以参考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小为11,就是桶大小设计为素数的应用(Hashtable扩容后不能保证还是素数)。HashMap采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。

这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。 


功能

HashMap的内部功能实现很多,本文主要从根据key获取哈希桶数组索引位置、put方法的详细执行、扩容过程三个具有代表性的点深入展开讲解。

. 确定哈希桶数组索引位置

不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现(方法一+方法二):

方法一:
static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算

这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

下面举例说明下,n为table的长度

  • 分析HashMap的put方法 

小结

(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。

(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。

(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。

(4) JDK1.8引入红黑树大程度优化了HashMap的性能。

(5) 还没升级JDK1.8的,现在开始升级吧。HashMap的性能提升仅仅是JDK1.8的冰山一角。

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java HashMap底层实现 的相关文章

随机推荐

  • 解决Port xxxx was already in use 端口被占用问题

    解决Port xxxx was already in use 端口被占用问题 通过快捷键Window R 键入cmd 进入命令行 1 输入如下命令查看端口被占用的进程 netstat ano findstr 8088 可以发现端口8088被
  • 程序员绩效总结_程序员吐槽,绩效工资与bug数量挂钩,网友:那就别敲代码

    近日 一个程序员提了一个问题 引起热议 该程序员表示 绩效跟bug数量挂钩合理吗 bug多就扣工资 连续几个月的话还辞退 对此 很多网友都认为完全不合理 如果是这样的话 那不敲代码岂不是就没有bug了 这完全有种为辞退找理由嘛 有句俗话说得
  • Linux系统中部署软件

    目录 1 Mysql 2 Redis 3 ZooKeeper 声明 致谢 1 Mysql 参考 CentOS7安装MySQL 补充 执行 rpm import https repo mysql com RPM GPG KEY mysql 2
  • node学习—validate数据库验证

    数据库验证 一 数据库验证 一 数据库验证 service studentService const Student require models Student const Op require sequelize const Class
  • STM32对接涂鸦wifi模块项目记录(智能插座完善版本)

    应项目需求 客户需要对接涂鸦平台 从了解平台到样品实际落地 还是挺方便的 做过的一个项目 人体感应智能插座项目 对接涂鸦云 硬件平台 STM32F103 WIFI模块 涂鸦WiFi 型号见文章说明 云平台 涂鸦云 更新项目原理图部分说明 更
  • Linux nmcli控制NetworkManager的命令行工具

    RHEL7 与 CentOS 7 以上的版本中默认的网络服务由 NetworkManager 提供 简称NM 这是动态控制及配置网络的守护进程 它用于保持当前网络设备及连接处于工作状态 同时也支持传统的 ifcfg 类型的配置文件 Netw
  • 盘点俄罗斯程序猿写的几款软件,你用过几个?最后1个是我的童年

    1 7zip 7 Zip 作者 abhishek prakash 是一款 开源 的 免费 软件 大多数源代码都基于 GNU LGPL 许可协议下发布 部分代码基于 BSD 3 句条款 BSD 3 clause 许可协议发布 可以在任何一台计
  • java private 构造函数_java-构造函数是否必须总是公开的?

    java 构造函数是否必须总是公开的 这个问题已经在这里有了答案 java中private构造函数的用法是什么 10个答案 我的第一个问题是 class Explain public Explain 构造函数应始终声明为公共吗 如果我创建2
  • Nginx 解决做反向代理时 静态资源图片、 js、css 访问不到

    在反向代理时添加另一个规则 反向代理 location proxy pass http localhost 9001 解决js css 访问不到的问题 location proxy pass http localhost 9001 prox
  • Lua 学习笔记:沙盒

    背景知识 Lua 给我的感觉是 各种内置函数和标准库的存在感都是比较强的 如果执行这句 for name in pairs G do print G end 就会把各种环境中已存在名称的打印出来 全局变量 比如字符串 VERSION 内置函
  • Linux系统简介和各发行版介绍

    一 Linux 简介 二 Linux和UNIX的关系及区别 UNIX 的发展历史 Linux 和 UNIX 的关系 区别 三 Linux 的发行版介绍 Linux各发行版简介 Debian 以社区的方式运作 Redhat 商业公司维护的发行
  • 使用 OpenSSL API 建立安全连接 - 双向认证

    使用 OpenSSL API 进行安全编程 一 概念 1 什么是 SSL SSL 是一个缩写 全称是 Secure Sockets Layer 它是支持在 Internet 上进行安全通信的标准 并且将数据密码术集成到了协议之中 数据在离开
  • mybatis log插件

    目前idea当中已经实施收费了 最近找了一个不收费的插件安装上重启一下就行了 点我下载提取码 sjc8
  • 如何提取视频的m3u8地址

    1 用360浏览器或者其他Chrome内核浏览器打开优酷网页 2 在播放页面按F12打开审核模式 3 点击如图图标模拟移动设备 4 设置模拟的设备 5 按F5刷新即可进入手机版网页 6 点击Network 7 点击Media 8 点击播放按
  • 2017年蓝桥杯B组C/C++省赛-分巧克力

    题目 题目链接 题解 二分 想到二分比实现二分要难点 可行解部分可以与不可行解部分完美地分隔开来 绿色部分是分成的巧克力比较小时都可以满足 而大于一定程度的时候就不可行了 所以可以将其抽象成小于可行 大于不可行的二分问题 在判断时 遍历全部
  • JAVA的分支结构

    分支结构 基本概述 当需要进行条件判断的时候 并且根据条件是否成立来执行某一段代码的时候 需要分支结构 1 if结构 if 布尔表达式 语句块 如果布尔表达式为true将执行的语句 如果布尔表达式的值为 true 则执行 if 语句中的代码
  • 四大私募量化策略解析——阿尔法、套利、期货CTA、高频交易

    近年来 随着证券市场规模的不断扩大 金融衍生产品不断推出 投资策略和盈利模式发生根本性改变 投资复杂程度日益提高 导致证券市场投资者的构成比例出现了相应的变化 专业投资管理人的占比越来越大 且有加速之势 另一方面 量化对冲投资策略以其中低风
  • Unity制作FPS Demo

    等到把这个Unity FPS Demo 僵尸杀手 完成后再详细补充一下 使用Unity制作FPS游戏的经历 今天做个标识
  • 算法入门Bu1:排序

    算法入门 BuBuBu 相关数据结构 栈 队列 链表 树 并差集 堆 图等 相关算法 排序 枚举 深度和广度优先搜索 图的遍历 图中最短路径算法 最小生成树算法 割点和割遍算法 二分图的最大匹配算法等 排序算法 简单的桶排序 特点 如果需要
  • Java HashMap底层实现

    HashMap 是 Java 使用频率最高的用于映射 键值对 处理的数据类型 JDK1 8 对 HashMap 底层的实现进行了优化 例如引入红黑树的数据结构和扩容的优化等 在JDK1 8以前HashMap是由数组 链表的数据结构组成的 J