这篇文章将会详细透彻地讲清楚 Java 的 HashMap,包括 hash 方法的原理、HashMap 的扩容机制、HashMap 的加载因子为什么是 0.75 而不是 0.6、0.8,以及 HashMap 为什么是线程不安全的,基本上 HashMap 的常见面试题,都会在这一篇文章里讲明白。
HashMap 是 Java 中常用的数据结构之一,用于存储键值对。在 HashMap 中,每个键都映射到一个唯一的值,可以通过键来快速访问对应的值,算法时间复杂度可以达到 O(1)。
HashMap 不仅在日常开发中经常用到,在面试中也是重点考察的对象。
以下是 HashMap 增删改查的简单例子:
1)增加元素:
将一个键值对(元素)添加到 HashMap 中,可以使用 put() 方法。例如,将名字和年龄作为键值对添加到 HashMap 中:
2)删除元素:
从 HashMap 中删除一个键值对,可以使用 remove() 方法。例如,删除名字为 "沉默" 的键值对:
3)修改元素:
修改 HashMap 中的一个键值对,可以使用 put() 方法。例如,将名字为 "沉默" 的年龄修改为 30:
为什么和添加元素的方法一样呢?这个我们后面会讲,先简单说一下,是因为 HashMap 的键是唯一的,所以再次 put 的时候会覆盖掉之前的键值对。
4)查找元素:
从 HashMap 中查找一个键对应的值,可以使用 get() 方法。例如,查找名字为 "沉默" 的年龄:
在实际应用中,HashMap 可以用于缓存、索引等场景。例如,可以将用户 ID 作为键,用户信息作为值,将用户信息缓存到 HashMap 中,以便快速查找。又如,可以将关键字作为键,文档 ID 列表作为值,将文档索引缓存到 HashMap 中,以便快速搜索文档。
HashMap 的实现原理是基于哈希表的,它的底层是一个数组,数组的每个位置可能是一个链表或红黑树,也可能只是一个键值对(后面会讲)。当添加一个键值对时,HashMap 会根据键的哈希值计算出该键对应的数组下标(索引),然后将键值对插入到对应的位置。
当通过键查找值时,HashMap 也会根据键的哈希值计算出数组下标,并查找对应的值。
01、hash 方法的原理
简单了解 HashMap 后,我们来讨论第一个问题:hash 方法的原理,对吃透 HashMap 会大有帮助。
来看一下 hash 方法的源码(JDK 8 中的 HashMap):
这段代码究竟是用来干嘛的呢?
将 key 的 hashCode 值进行处理,得到最终的哈希值。
怎么理解这句话呢?不要着急。
我们来 new 一个 HashMap,并通过 put 方法添加一个元素。
来看一下 put 方法的源码。
看到 hash 方法的身影了吧?
hash 方法的作用
前面也说了,HashMap 的底层是通过数组的形式实现的,初始大小是 16(这个后面会讲),先记住。
也就是说,HashMap 在添加第一个元素的时候,需要通过键的哈希码在大小为 16 的数组中确定一个位置(索引),怎么确定呢?
为了方便大家直观的感受,我这里画了一副图,16 个方格子(可以把它想象成一个一个桶),每个格子都有一个编号,对应大小为 16 的数组下标(索引)。
现在,我们要把 key 为 “chenmo”,value 为“沉默”的键值对放到这 16 个格子中的一个。
怎么确定位置(索引)呢?
我先告诉大家结论,通过这个与运算 (n - 1) & hash
,其中变量 n 为数组的长度,变量 hash 就是通过 hash()
方法计算后的结果。
那“chenmo”这个 key 计算后的位置(索引)是多少呢?
答案是 8,也就是说 map.put("chenmo", "沉默")
会把 key 为 “chenmo”,value 为“沉默”的键值对放到下标为 8 的位置上(也就是索引为 8 的桶上)
这样大家就会对 HashMap 存放键值对(元素)的时候有一个大致的印象。其中的一点是,hash 方法对计算键值对的位置起到了至关重要的作用。
回到 hash 方法:
- 参数 key:需要计算哈希码的键值。
key == null ? 0 : (h = key.hashCode()) ^ (h >>> 16)
:这是一个三目运算符,如果键值为 null,则哈希码为 0(依旧是说如果键为 null,则存放在第一个位置);否则,通过调用hashCode()
方法获取键的哈希码,并将其与右移 16 位的哈希码进行异或运算。^
运算符:异或运算符是 Java 中的一种位运算符,它用于将两个数的二进制位进行比较,如果相同则为 0,不同则为 1。h >>> 16
:将哈希码向右移动 16 位,相当于将原来的哈希码分成了两个 16 位的部分。- 最终返回的是经过异或运算后得到的哈希码值。
这短短的一行代码,汇聚不少计算机巨佬们的聪明才智。
理论上,哈希值(哈希码)是一个 int 类型,范围从-2147483648 到 2147483648。
前后加起来大概 40 亿的映射空间,只要哈希值映射得比较均匀松散,一般是不会出现哈希碰撞(哈希冲突会降低 HashMap 的效率)。
但问题是一个 40 亿长度的数组,内存是放不下的。HashMap 扩容之前的数组初始大小只有 16,所以这个哈希值是不能直接拿来用的,用之前要和数组的长度做与运算(前文提到的 (n - 1) & hash
,有些地方叫取模预算,有些地方叫取余运算),用得到的值来访问数组下标才行。
取模运算 VS 取余运算 VS 与运算
那这里就顺带补充一些取模预算/取余运算和与运算的知识点哈。
取模运算(Modulo Operation)和取余运算(Remainder Operation)从严格意义上来讲,是两种不同的运算方式,它们在计算机中的实现也不同。
在 Java 中,通常使用 % 运算符来表示取余,用 Math.floorMod()
来表示取模。
- 当操作数都是正数的话,取模运算和取余运算的结果是一样的。
- 只有当操作数出现负数的情况,结果才会有所不同。
- 取模运算的商向负无穷靠近;取余运算的商向 0 靠近。这是导致它们两个在处理有负数情况下,结果不同的根本原因。
- 当数组的长度是 2 的 n 次方,或者 n 次幂,或者 n 的整数倍时,取模运算/取余运算可以用位运算来代替,效率更高,毕竟计算机本身只认二进制嘛。
我们通过一个实际的例子来看一下。
标签:Java,HashMap,16,源码,键值,哈希,hash,运算 From: https://blog.csdn.net/sunyan5211314/article/details/142165891