HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变
2. HashMap的数据结构在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外,HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体
3. HashMap 基于 Hash 算法实现当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标存储元素
如果出现hash值相同的key,此时有两种情况
如果key相同,则覆盖原始值如果key不同(出现冲突),则将当前的key-value放入链表中,Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn) 获取数据元素
直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值
二、 HashMap在JDK1.7和JDK1.8中底层实现不同
在Java中,保存数据有两种比较简单的数据结构:数组和链表数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突 1. HashMap JDK1.8之前
JDK1.8之前采用的是拉链法
将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可
2. HashMap JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
3. JDK1.7 VS JDK1.8 比较
JDK1.8主要解决或优化了以下问题
resize 扩容优化引入了红黑树,目的是避免单条链表过长而影响查询效率,解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题
| 不同 | 1.7 | 1.8 |
|---|---|---|
| 存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 初始化方式 | 单独函数: inflateTable() | 直接集成到了扩容函数 resize() 中 |
| hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
| 存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 <8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 |
| 插入数据方式 | 头插法(先将原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
| 扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
当我们put的时候,判断hashMap的存储容量是否大于当前容量*0.75(loadFatory–加载因子),如果大于进行扩容resize为原来的一倍
计算 key 的 hash 值,这里调用了 hash 方法, hash 方法实际是让key.hashCode() 与 key.hashCode()>>>16 进行异或操作,高16bit补0,一个数和0异或不变
以 hash 函数做异或运算
高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞
因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) &hash ,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位
数组大小为2的幂时性能最高,减少哈希碰撞
数组长度为16-1(2的4次方-1),使用不同的hashCode进行与运算,结果是9和8
数组长度为15-1,使用不同的hashCode进行与运算,结果是8和8
当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,数组可以使用的位置比数组长度小了很多,增加了碰撞的几率,减慢了查询的效率!
当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了
通过hash计算出hash值,哈希没有碰撞直接存入
哈希碰撞,通过equal比较key值,相同则覆盖原始值,返回旧的value
计算的key值不相同,则将当前的key-value放入链表中Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
putVal方法执行流程图
链表法
将相同hash值的对象组织成一个链表放在hash值对应的槽位
开放地址法
通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位
相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4 (即2的四次方16)要远小于int类型的范围,让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己
右移16位进行异或运算(高低位异或)
}
相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动) 不能使用哈希值直接作为table的下标
hashCode() 方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过 hashCode() 计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置
解决方案
HashMap自己实现了自己的 hash() 方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均在保证数组长度为2的幂次方的时候,使用 hash() 运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题 四、 HashMap的扩容操作 jdk1.8扩容操作
- 在jdk1.8中,resize方法是在hashMap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容每次扩展的时候,都是扩展2倍扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方
- 在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置对其进行分发,但在1.8版本中,则是根据要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上



