栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

HashMap实现原理和扩容机制

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

HashMap实现原理和扩容机制

一、HashMap实现原理 1. HashMap概述

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.71.8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数: inflateTable()直接集成到了扩容函数 resize() 中
hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 <8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式头插法(先将原位置的数据移到后1位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)
三、 HashMap的put方法的具体流程 流程

    当我们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版本的一个优化的地方
jdk1.7扩容操作
    在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置对其进行分发,但在1.8版本中,则是根据要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/749203.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号