HashMap使用及底层实现

目录

(一)Map接口

(1)Map接口是什么

(2)Map接口的特性

(3)Map接口中的方法

(二)HashMap

(1)HashMap与Map接口的关系

(2)HashMap的底层结构

2.1哈希表

2.2哈希函数,关键字,Hash码

2.3向hash表插入/搜索

(3)冲突

3.1冲突概念

3.2冲突避免

3.2.1设计合理的哈希函数

3.2.2负载因子

3.2.3HashMap中的扩容机制

3.3冲突解决

(1)闭散列:

(2)开散列

1.树化

(三)总结


(一)Map接口

(1)Map接口是什么

Map是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关,如TreeMap和HashMap

下图为Map接口与HashMap与TreeMap关系

(2)Map接口的特性

1.Map是一个接口类,该类没有继承自 Collection ,该类中存储的是 <K,V> 结构的键值对,并且 K一定是唯一的,不能重复。
2. Map 中存放键值对的 Key 是唯一的, value 是可以重复的
3. Map 中键值对的 Key 不能直接修改 value 可以修改,如果要修改 key ,只能先将该 key 删除掉,然后再来进行 重新插入。

(3)Map接口中的方法

当然Map是一个接口,其中方法的具体实现与具体的实例化子类有关,但完成功能大体相同。
V get (Object key) :返回 key 对应的 value
V getOrDefault (Object key, V defaultValue) :返回 key 对应的 value key 不存在,返回默认值
V put (K key, V value) :设置 key 对应的 value
V remove (Object key):删除 key 对应的映射关系
Set<K> keySet () :返回所有 key 的不重复集合
Collection<V> values ():返回所有 value 的可重复集合
Set<Map.Entry<K, V>> entrySet () :返回所有的 key-value 映射关系
boolean containsKey (Object key):判断是否包含 key
boolean containsValue (Object value):判断是否包含 value

(二)HashMap

(1)HashMap与Map接口的关系

HashMap继承了Map接口,并重写了Map接口中的方法

下图中为HashMap的继承关系

(2)HashMap的底层结构

2.1哈希表

HashMap的底层结构是哈希表(散列表,哈希桶),哈希表通常采用“数组+链表+红黑树”的结构来实现。具体来说,哈希表是一个数组,数组的每个元素都是一个指针,指向一个链表的头节点。这个链表用于存储所有哈希码(哈希值)相同的键值对。

哈希表大概是如图所示结构

一般HashMap的初始容量和扩容都是2的次幂,这样可以有效减少hash冲突,具体原因与HashMap中的hash()算法有关。

下图计算出 h 之后再与数组长度-1进行与运算,计算出储存下标的位置。

  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//>>>16后相异或也是为了减少hash冲突
    }

2.2哈希函数,关键字,Hash码

给定一个函数H(key),对一个给定的x,带入函数后得到的值为y,

那么这个函数是哈希函数,这个x是关键字,y是哈希码

例:将哈希函数设置为hash(key) = key %10,那么这个函数叫做哈希函数,key叫做关键字,hash(key)叫做哈希码 。

2.3向hash表插入/搜索

插入元素 :根据待插入元素的关键码,以哈希函数计算出该元素的哈希码,利用哈希码找到存储位置并按此位置进行存放
搜索元素 :对元素的关键码进行同样的计算,把求得的哈希码通过某种方式找到元素的存储位置,在结构中按此位置取元素比较,若相等,则搜索成功。
例:将哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小
将key = 1,4,7的关键字放入如下哈希表中如下图
过程:(1) 4(key)%10 = 4(hash(key))得到hash码 4
(2)  4(哈希码)即为该元素存储位置,按此位置进行存放
当然,真正计算出哈希码之后,一般还需要做进一步处理(比如扰动函数),不会和这个一样直接确定位置。
HashMap在多线程的情况下调用putVal方法进行插入操作时,可能会发生数据覆盖的问题,
如线程A,B通过哈希函数算出的存储位置一致,线程A判断完成后挂起,线程B判断完后将数据放入,线程A再将数据放入就会把B的数据覆盖,此时可以使用线程安全的ConcurrentHashMap.

(3)冲突

3.1冲突概念

对于两个数据元素的关键字 i和 j ,有  i != j  ,但有: Hash(i) == Hash(j) ,即: 不同关键字通过相同哈希哈数计算出相同的哈希地址 ,该种现象称为哈希冲突或哈希碰撞
把具有不同关键码而具有相同哈希地址的数据元素称为 同义词

例:两个关键字 2 和 12他们两个是不相等的,但通过哈希函数hash(key) = key%10 ,计算出的哈希码相同的,这种现象成为哈希冲突。

3.2冲突避免

由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一
个问题, 冲突的发生是必然的 ,但我们能做的应该是尽量的 降低冲突率
3.2.1设计合理的哈希函数
设计原则:
1.哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 m-1 之间
2.哈希函数计算出来的地址能均匀分布在整个空间中
3.哈希函数应该比较简单

常见的哈希函数设计有: 直接定制法,除留余数法,折叠法,数学分析法等.....

3.2.2负载因子

负载因子概念: 负载因子是散列表装满程度的标志因子,由于表是定长,负载因子随着填入表中元素的个数成正比,填入的越多,负载因子越高

负载因子计算:负载因子 = 填入表中元素的个数 / 哈希表的长度。

负载因子与冲突率关系图

可见冲突率随负载因子的升高而升高,所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率,已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小

3.2.3HashMap中的扩容机制

在Java的HashMap中,默认情况下,HashMap的负载因子为0.75f。这意味着,当HashMap中的元素数量达到容量(初始容量或扩容后的容量)的75%时,HashMap会进行扩容操作。

下面是HashMap的扩容机制部分源码,我在其中加入了详细的注释,需要注意的是,新的数组容量不是原来旧数组容量的2倍,而是旧的扩容阈值的2倍,扩容是根据扩容阈值扩容的

比如负载因子是0.75,如果旧数组大小是16(16是hash表的初始容量),那么旧扩容阈值是16*0.75 = 12,那么触发扩容操作时新的数组容量为12*2 = 24而不是16*2 = 32。

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//当前HashMap的table数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//当前数组的长度
        int oldThr = threshold;//扩容阈值,当数组中的数超过这个值会发生扩容
        int newCap, newThr = 0;//新的数组容量和扩容阈值
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {//如果旧数组大于数组最大容量1>>30(2的30次方)
                threshold = Integer.MAX_VALUE;
                return oldTab;//返回旧的数组
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//确保新的容量没有超过HashMap允许的最大容量并且旧的容量大于等于默认初始容量
                newThr = oldThr << 1; //将新阈值设置为旧阈值二倍
        }
        else if (oldThr > 0) //如果旧阈值大于0
            newCap = oldThr;//将旧阈值赋值给新的数组容量(注意这是已经扩大过2倍的旧阈值)
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        
        

3.3冲突解决

如果发生了冲突之后,我们怎么解决冲突呢,冲突解决有两大方法,闭散列开散列。

(1)闭散列:

也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置中去。那么如何寻找下一个空位置。

1.线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
2.二次探测
找下一个空位置的方法为:{H_{i}} = (H_{0}+i^{2})  %m, 或者:{H_{i}} = (H_{0}-i^{2})%m其中: i = 1,2,3… , 是通过散列函数Hash(x) 对元素的关键码 key 进行计算得到的位置,m是表的大小。
(2)开散列
开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址 ,具有相同地址的关键码归于同一子 集合,每一个子集合称为一个桶 ,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
还记得我们开头画的那个哈希表的图,如下便是哈希表用开散列法解决冲突后的样子
1.树化
如果链表长度过长,超过8时,链表遍历的速度就会较慢,链表就会数化,变为红黑树,提升查询效率。
如下图,jdk将树化阈值设置为8
当链表长度缩短为6时,红黑树就会重新转换为链表
如下图jdk将链化阈值设置为6
为什么树化阈值设置成8,链化阈值设置为6,而不是都设置为8或者6呢?

原因:如果链化阈值也设置为8,那么在元素数量在7和8之间波动时,链表和红黑树之间会发生频繁的转换,这不仅会浪费CPU资源,还会影响性能。

(三)总结

这篇文章讲到了

1.Map接口的特性

2.Map接口的方法

3.哈希表,哈希函数,关键字,哈希码

4.冲突的概念,冲突的避免(设计哈希函数,调整负载因子,适时扩容),冲突的解决(闭散列,开散列)

  • 25
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值