热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JavaHashMap原理解析

本文分析HashMap的实现原理。数据结构(散列表)HashMap是一个散列表(也叫哈希表),用来存储键值对(

本文分析HashMap的实现原理。

数据结构(散列表)

HashMap是一个散列表(也叫哈希表),用来存储键值对(key-value)映射。散列表是一种数组和链表的结合体,结构图如下:

来自百度百科的哈希表结构图

简单来说散列表就是一个数组(上图纵向),数组的每个元素是一个链表(上图横向),类似二维数组。链表的每个节点就是我们存储的key-value数据(源码中将key和value封装成Entry对象作为链表的节点)。


哈希算法

对于散列表,不管是存值还是取值,都需要通过Key来定位散列表中的一个具体的位置(即某个链表的某个节点),计算这个位置的方法就是哈希算法。

大概过程是这样的:

  1. 用Key的hash值对数组长度做取余操作得到一个整数,这个整数作为数组中的索引得到这个索引位置的链表。
  2. 得到链表之后,就可以存值和取值了。
    如果是存值,直接把数据插入到链表的头部或者尾部即可(或者已存在就替换);
    如果是取值,就遍历链表,通过key的equals方法找到具体的节点。

例如一个key-value对要存到上图的散列表里,假设key的哈希值是17,由图可知(纵向)数组长度是16,那么17对16取余结果是1,数组中索引1位置的链表是 1->337->353 ,所以这个key-value对存储到这个链表里面(插到头还是尾可能不同Java版本不一样)。如果是取值,就遍历这个链表,由于这个链表每个节点的key的哈希值都一样,所以根据equals方法来确定具体是哪个节点。

通过上面的哈希算法,可以有如下结论:

  • 不同的key具体相同的哈希值叫做哈希冲突,HashMap解决哈希冲突的方法是链表法,将具有相同哈希值的key放在同一个链表中,然后利用key类的equals方法来确定具体是哪个节点。
  • Key的唯一性是通过哈希值和equals方法共同决定的,所以想要用一个类作为HashMap的键,必须重写这个类的hashCode和equals方法。同理,HashSet是基于HashMap实现的,它没有重复元素的特点是利用HashMap没有重复键实现的。所以,Set集合里面的元素类,也必须同时实现hashCode方法和equals方法。
  • HashMap存储的数据是无序的。


为什么HashMap大小是2的整数次幂的时候效率最高

哈希算法主要分两步操作:1.通过哈希值定位一个链表; 2.遍历链表,通过equals方法找到具体节点。为了使哈希算法效率最高,应该尽量让数据在哈希表中均匀分布,因为那样可以避免出现过长的链表,也就降低了遍历链表的代价。
如何保证均匀分布?前面的哈希算法说到,通过取余操作将Key的哈希值转换成数组下标,这样可以认为是均匀的。但是,源码中并没有直接用%操作符取余,而是使用了更高效的与运算,源码如下:

/*** Returns index for hash code h.*/
static int indexFor(int h, int length) {// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";return h & (length-1);
}

这样就多了一些限制,因为只有当length是2的整数次幂的时候,h & (length-1) = h % length才成立。当然,如果length不是2的整数次幂,h & (length-1)的结果也一定比length小,将Key转换成数组下标也没什么问题,但是,这样会导致元素分布不均匀严重影响散列表的访问效率。看下面的一个示例代码:

示例代码

解释一下图中的代码,随机生成一组Key,然后利用与运算,把key全部转换成一个数组容量的索引,这样就得到一组索引值,这组索引中不相同的值越多,说明分布越均匀,输出结果的result就是 “这组索引中不相同的值的数量”。
从运行结果来看,容量是64的时候相比于其他几个容量大小,分布是最均匀的。容量是65的时候,每次结果都是2,原因很简单,当容量是65的时候,下标=h&64,64的二进制是1000000,很明显,与它进行与运算的结果只有两种情况,0和64,也就是说,如果HashMap大小被指定成65,对于任意Key,只会存储到散列表数组的第0个或第64个链表中,浪费了63个空间,同时也导致0和64两个链表过长,取值的时候遍历链表的代价很高。容量66和67的结果是4同理。如果容量是64,那么下标=h&63,63的二进制是111111,每一位都是1,好处就是对于任意Key,与63做与运算的结果可能是1-63的任意数,很多Key的话自然就能分布均匀。
通过这个示例代码的分析就可以找到一个规律了,容量length=2^n 是分布最均匀,因为length-1的二进制每一位都是1;相反的length=2^n+1是分布最不均匀的,因为length-1的二进制中的1数量最少。

结论:HashMap大小是2的整数次幂的时候效率最高,因为这个时候元素在散列表中的分布最均匀。

从上面的分析来看,使用与运算虽然效率高了,但是增加了使用限制,如果用%取余的做法,那么对于任何大小的容量都能做到均匀分布,可以把图中代码int a = keySet[j] & (c - 1); 改成 int a = keySet[j] % c;试一下。


HashMap的容量

通过上面的分析,容量是2的整数次幂的时候效率最高,那么很容易想到,如果随着数据量的增长,HashMap需要扩容的时候是2倍扩容,区别于ArrayList的1.5倍扩容。
那么什么时候扩容呢?首先说明一下,我们所说的HashMap的容量是指散列表中数组的大小,这个大小不能决定HashMap能存多少数据,因为只要链表足够长,存多少数据都没问题。但是,数据量很大的时候,如果数组太小,就会导致链表很长,get元素的效率就会降低,所以我们应该在适当的时候扩容。源码默认的做法是,当数据量达到容量的75%的时候扩容,这个值称为负载因子,75%应该是大量实验后统计得到的最优值,没有特殊情况不要通过构造方法指定为其他值。
扩容是有代价了,会导致所有已存的数据重新计算位置,所以,和ArrayList一样,当知道大概的数据量的时候,可以指定HashMap的大小尽量避免扩容,指定大小要注意75%这个负载因子,比如数据量是63个的话,HashMap的大小应该是128而不是64。

对于容量的计算,源码已经封装好了一个方法

/*** Returns a power of two size for the given target capacity.*/
static final int tableSizeFor(int cap) {int n &#61; cap - 1;n |&#61; n >>> 1;n |&#61; n >>> 2;n |&#61; n >>> 4;n |&#61; n >>> 8;n |&#61; n >>> 16;return (n <0) ? 1 : (n >&#61; MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n &#43; 1;
}

此方法在HashMap的构造方法中被调用&#xff0c;所以指定容量的时候无需自己计算&#xff0c;比如数据量是63&#xff0c;直接new HashMap<>(63)即可。


HashMap的遍历

前面提到一点&#xff0c;散列表中的链表的节点是Entry对象&#xff0c;通过Entry对象可以得到Key和Value。HashMap的遍历方法有很多&#xff0c;大概可以分为3种&#xff0c;分别是通过map.entrySet()、map.keySet()、map.values()三种方式遍历。比较效率的话&#xff0c;map.values()方式无法得到key&#xff0c;这里不考虑。比较map.entrySet()和map.keySet()的话&#xff0c;结合散列表的结构特点&#xff0c;很明显map.entrySet()直接遍历Entry集合&#xff08;所有链表节点&#xff09;取出Key和Value即可&#xff08;一次循环&#xff09;&#xff0c;map.keySet()遍历的是Key&#xff0c;得到Key之后在通过Key去遍历相应的链表找到具体的节点&#xff08;多个循环&#xff09;&#xff0c;所以前者效率高。


扩展&#xff1a;LinkedHashMap和LruCatch

对于LinkedHashMap的理解&#xff0c;我觉得一张图就够了&#xff1a;

LinkedHashMap结构图

在散列表的基础上加上了双向循环链表&#xff08;图中黄色箭头和绿色箭头&#xff09;&#xff0c;所以可以拆分成一个散列表和一个双向链表&#xff0c;双向链表如下&#xff1a;

双向循环链表图

上面两张图片来自&#xff1a;https://www.cnblogs.com/xiaoxi/p/6170590.html

然后使用散列表操作数据&#xff0c;使用双向循环链表维护顺序&#xff0c;就实现了LinkedHashMap。

LinkedHashMap有一个属性可以设置两种排序方式&#xff1a;

private final boolean accessOrder;

false表示插入顺序&#xff0c;true表示最近最少使用次序&#xff0c;后者就是LruCatch的实现原理。

LinkedHashMap和LruCatch的具体实现细节这里就不分析了。


转:https://www.cnblogs.com/developerzjy/p/11084191.html



推荐阅读
  • 深入解析Android自定义View面试题
    本文探讨了Android Launcher开发中自定义View的重要性,并通过一道经典的面试题,帮助开发者更好地理解自定义View的实现细节。文章不仅涵盖了基础知识,还提供了实际操作建议。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • Explore how Matterverse is redefining the metaverse experience, creating immersive and meaningful virtual environments that foster genuine connections and economic opportunities. ... [详细]
  • Explore a common issue encountered when implementing an OAuth 1.0a API, specifically the inability to encode null objects and how to resolve it. ... [详细]
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 本文将介绍如何编写一些有趣的VBScript脚本,这些脚本可以在朋友之间进行无害的恶作剧。通过简单的代码示例,帮助您了解VBScript的基本语法和功能。 ... [详细]
  • Java 中的 BigDecimal pow()方法,示例 ... [详细]
  • 本文介绍如何利用动态规划算法解决经典的0-1背包问题。通过具体实例和代码实现,详细解释了在给定容量的背包中选择若干物品以最大化总价值的过程。 ... [详细]
  • 本文详细探讨了Java中的24种设计模式及其应用,并介绍了七大面向对象设计原则。通过创建型、结构型和行为型模式的分类,帮助开发者更好地理解和应用这些模式,提升代码质量和可维护性。 ... [详细]
  • 本文基于刘洪波老师的《英文词根词缀精讲》,深入探讨了多个重要词根词缀的起源及其相关词汇,帮助读者更好地理解和记忆英语单词。 ... [详细]
  • 本文深入探讨了 Java 中的 Serializable 接口,解释了其实现机制、用途及注意事项,帮助开发者更好地理解和使用序列化功能。 ... [详细]
  • 深入解析:手把手教你构建决策树算法
    本文详细介绍了机器学习中广泛应用的决策树算法,通过天气数据集的实例演示了ID3和CART算法的手动推导过程。文章长度约2000字,建议阅读时间5分钟。 ... [详细]
author-avatar
看破红尘红尘看破_728
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有