您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

HashMap的底层数据结构

时间:2023-09-15 13:32:19  来源:今日头条  作者:微风01
在 JDK1.8 中,HashMap 还引入了一个新的概念,叫做负载因子(load factor),它是指哈希表中键值对的数量与数组长度的比值。当键值对的数量超过了负载因子与数组长度的乘积时,就会触发扩容操作,HashMap 会自动将数组长度扩大一倍,并将原来的键值对重新分配到新的数组中。这样做的目的是为了保证散列表的性能,因为当负载因子过高时,散列表的性能会急剧下降。

一、HashMap基础机构

HashMap 由数组和链表(或红黑树)组成。数组是 HashMap 的主体,链表和红黑树则是为了解决哈希冲突而存在的。数组中的每个元素都是一个单向链表的头结点,每个链表都是由若干个 Node 节点组成的,每个节点都包含了键值对的信息,以及指向下一个节点的指针。当多个键映射到同一个位置时,它们会被存储在同一个链表中(或者是同一个红黑树中)。当链表长度超过阈值(默认为 8)时,链表就会被转换成红黑树,这样可以提高查找效率。

在 JDK1.8 中,HashMap 还引入了一个新的概念,叫做负载因子(load factor),它是指哈希表中键值对的数量与数组长度的比值。当键值对的数量超过了负载因子与数组长度的乘积时,就会触发扩容操作,HashMap 会自动将数组长度扩大一倍,并将原来的键值对重新分配到新的数组中。这样做的目的是为了保证散列表的性能,因为当负载因子过高时,散列表的性能会急剧下降。

二、HashMap的底层数据结构

解答:在jdk1.8以前,HashMa采用链表+数组,自Jdk1.8以后,HashMap采用链表+数组+红黑树。在下图中横链(0-15)表中表示数组,竖(1-8)表示链表,在数组长度超过8之后,hashmap将数组自动转为红黑树。

HashMapJDK1.8链表和红黑树转化

三、JDK1.8对hash算法和寻址算法如何优化的?

1、对Hash值算法的优化

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

有一个key的Hash_1值:

Hash_1: 1111 1111 1111 1111 1111 1010 0111 1100
 
h >>> 16 // 表示对该hash值右移16位

右移后的结果Hash_2为:

Hash_2: 0000 0000 0000 0000 1111 1111 1111 1111

对上述Hash_1和Hash_2的两个值进行异或

Hash_1: 1111 1111 1111 1111 1111 1010 0111 1100
Hash_2: 0000 0000 0000 0000 1111 1111 1111 1111
=====>: 1111 1111 1111 1111 0000 0101 1000 0011 =====> 转为10进制int值,这个值就是这个key的hash值

hash算法的优化:对每个hash值,在它的低16位中,让高低16位进行异或,让它的低16位同时保持了高低16位的特征,尽量避免一些hash值后续出现冲突,大家可能会进入数组的同一位置。

2、对寻址算法的优化

(p = tab[i = (n - 1) & hash] 
 
 // (n-1) & hash ==> 数组里的一个位置

hash & (n-1) 效果是跟hash对n取模是一样的,但是与运算的性能要比hash对n取模要高很多。数组的长度会一直是2的n次方,只要他保持数组长度是2的n次方。

  • 寻址为什么不用取模?

对于上面寻址算法,由于计算机对比取模,与运算会更快。所以为了效率,HashMap 中规定了哈希表长度为 2 的 k 次方,而 2^k-1 转为二进制就是 k 个连续的 1,那么 hash & (k 个连续的 1) 返回的就是 hash 的低 k 个位,该计算结果范围刚好就是 0 到 2^k-1,即 0 到 length - 1,跟取模结果一样。

也就是说,哈希表长度 length 为 2 的整次幂时, hash & (length - 1) 的计算结果跟 hash % length 一样,而且效率还更好。

  • 为什么不直接用 hashCode() 而是用它的高 16 位进行异或计算新 hash 值?#

int 类型占 32 位,可以表示 2^32 种数(范围:-2^31 到 2^31-1),而哈希表长度一般不大,在 HashMap 中哈希表的初始化长度是 16(HashMap 中的 DEFAULT_INITIAL_CAPACITY),如果直接用 hashCode 来寻址,那么相当于只有低 4 位有效,其他高位不会有影响。这样假如几个 hashCode 分别是 210、220、2^30,那么寻址结果 index 就会一样而发生冲突,所以哈希表就不均匀分布了。

寻址算法的优化:用与运算替代取模,提升性能。(由于计算机对比取模,与运算会更快)

四、HashMap是如何解决hash碰撞问题

hash冲突问题,链表+红黑树,O(n)和O(logN)。

hashmap采用的就是链地址法(拉链法),jdk1.7中,当冲突时,在冲突的地址上生成一个链表,将冲突的元素的key,通过equals进行比较,相同即覆盖,不同则添加到链表上,此时如果链表过长,效率就会大大降低,查找和添加操作的时间复杂度都为O(n);但是在jdk1.8中如果链表长度大于8,链表就会转化为红黑树,时间复杂度也降为了O(logn),性能得到了很大的优化。

HashMapJDK1.8链表和红黑树转化

五、HashMap是如何进行扩容的

HashMap底层是一个数组,当这个数组满了之后,他就会自动进行扩容,变成一个更大数组。

1、JDK1.7下的扩容机制

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

代码中可以看到,如果原有table长度已经达到了上限,就不再扩容了。如果还未达到上限,则创建一个新的table,并调用transfer方法:

/**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;              //注释1
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity); //注释2
                e.next = newTable[i];                  //注释3
                newTable[i] = e;                       //注释4
                e = next;                              //注释5
            }
        }
    }

transfer方法的作用是把原table的Node放到新的table中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致环状节点。

2、JDK1.8下的扩容机制

源码如下:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // 记录原来的数组长度
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold // 重新计算TREEIFY_THRESHOLD
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        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);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {  // 重新计算原来链表中的值的hash值在新表对应的hash值
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)  // 如果元素e的下一个位置没有值,则说明可以存放元素
                        newTab[e.hash & (newCap - 1)] = e; 
                    else if (e instanceof TreeNode) // 如果已经是红黑树的节点,那就对其重新划分
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // loHead: 下标不变情况下的链表头
                        // loTAIl: 下标不变情况下的链表尾
                        // hiHead: 下标改变情况下的链表头
                        // hiTail: 下标改变情况下的链表尾
                        // 如果
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) { // 元素e的最新hash如果与原来的值与计算之后如果值为0,就说明是使用原来的index
                                // 尾插法插入元素e
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                // 与运算不等于0则说明使用新的index
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。

数组长度为16时,有两个keyA和keyB。

KeyA:
n-1:   0000 0000 0000 0000 0000 0000 0000 1111
hash1: 1111 1111 1111 1111 0000 1111 0000 0101
&结果:  0000 0000 0000 0000 0000 0000 0000 0101 = 5

KeyB:
n-1:   0000 0000 0000 0000 0000 0000 0000 1111 
hash1: 1111 1111 1111 1111 0000 1111 0001 0101
&结果:  0000 0000 0000 0000 0000 0000 0000 0101 = 5

在数组长度为16的时候,他们两个hash值冲突会使用拉链发解决冲突。

当数组长度扩容到32之后,需要重新对每个hash值进行寻址,也就是每个hash值跟新的数组length-1 进行操作。

KeyA:
n-1:   0000 0000 0000 0000 0000 0000 000*1* 1111
hash1: 1111 1111 1111 1111 0000 1111 0000 0101
&结果:  0000 0000 0000 0000 0000 0000 0000 0101 = 5

KeyB:
n-1:   0000 0000 0000 0000 0000 000*1* 0000 1111 
hash1: 1111 1111 1111 1111 0000 1111 0001 0101
&结果:  0000 0000 0000 0000 0000 000*1* 0000 0101 = 21

判断二进制结果是否多出一个bit的1,如果没有多,那就用原来的index,如果多出来了那就用index+oldCap,通过这个方式,避免了rehash的时候,用每个hash对新数组的length取模,取模性能不高,位运算性能比较高。



Tags:HashMap   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Search: HashMap  点击:(11)  评论:(0)  加入收藏
HashMap:Java中的高效数据结构
HashMap是Java中常用的数据结构之一,它实现了Map接口,并且提供了快速的查找、插入和删除操作。HashMap的底层数据结构是数组和链表(或红黑树)的组合,这种数据结构被称为哈希表(Has...【详细内容】
2023-11-24  Search: HashMap  点击:(329)  评论:(0)  加入收藏
HashMap的底层数据结构
在 JDK1.8 中,HashMap 还引入了一个新的概念,叫做负载因子(load factor),它是指哈希表中键值对的数量与数组长度的比值。当键值对的数量超过了负载因子与数组长度的乘积时,就会...【详细内容】
2023-09-15  Search: HashMap  点击:(239)  评论:(0)  加入收藏
HashMap 的基础结构,必须掌握!
HashMap 是一种散列表,它存储的内容是键值对(key-value)映射。在 HashMap 中,每个键(key)映射到一个值(value)。散列表的工作原理是:当通过 put() 方法将键值对存储在 HashMap...【详细内容】
2023-09-14  Search: HashMap  点击:(277)  评论:(0)  加入收藏
HashMap 是怎么解决哈希冲突的?
前言 今天来分享一道比较好的面试题,“HashMap 是怎么解决哈希冲突的?”对于这个问题,我们一起看看考察点和比较好的回答吧!考察点 现在的企业级开发中HashMap几乎是...【详细内容】
2023-09-11  Search: HashMap  点击:(198)  评论:(0)  加入收藏
搞懂hashMap底层原理
说明hashMap在java1.7和java1.8版本中有做一些调整,我们本篇只说java1.7的hashMap。数据结构hashMap的数据结构是由数组和链表组成,table是一个存放Entry对象的数组,每个Entry...【详细内容】
2023-08-03  Search: HashMap  点击:(106)  评论:(0)  加入收藏
HashMap线程不安全体现在哪里?
HashMap线程不安全体现在哪里?如果你到现在还不清楚赶紧看下去,明明白白补一补~。在Java中,HashMap是一种常用的数据结构,它以键值对的形式存储和管理数据。然而,由于HashMap在...【详细内容】
2023-04-27  Search: HashMap  点击:(291)  评论:(0)  加入收藏
如何实现线程安全的HashMap?
要实现线程安全的 HashMap,可以考虑以下几种方法: 使用 ConcurrentHashMap:ConcurrentHashMap 是线程安全的 HashMap 实现,采用了分段锁的机制,可以提高并发性能。 使用 Collecti...【详细内容】
2023-03-21  Search: HashMap  点击:(266)  评论:(0)  加入收藏
三分钟轻松搞懂 HashMap 死循环问题!
HashMap 死循环发生在 JDK 1.7 版本中,形成死循环的原因是 HashMap 在 JDK 1.7 使用的是头插法,头插法 + 链表 + 多线程并发 + HashMap 扩容,这几个点加在一起就形成了 HashMap...【详细内容】
2023-01-31  Search: HashMap  点击:(256)  评论:(0)  加入收藏
HashMap核心原理分析
学习目标1、hash冲突的解决办法有哪几种2、HashTable、hashmap、CHM三者之间的区别3、HashMap的默认长度是多少?默认扩容因子是多少?4、HashMap它是怎么解决hash冲突的5、Hash...【详细内容】
2022-09-13  Search: HashMap  点击:(134)  评论:(0)  加入收藏
▌简易百科推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  步步运维步步坑    Tags:架构   点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27    InfoQ  Tags:架构模式   点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  哈啰技术  微信公众号  Tags:架构   点击:(10)  评论:(0)  加入收藏
DDD 与 CQRS 才是黄金组合
在日常工作中,你是否也遇到过下面几种情况: 使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能扛...【详细内容】
2024-03-27  dbaplus社群    Tags:DDD   点击:(11)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13    阿里云开发者  Tags:高并发   点击:(6)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  二进制跳动  微信公众号  Tags:架构设计   点击:(36)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  ijunfu  今日头条  Tags:SpringBoot   点击:(10)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  547蓝色星球    Tags:架构   点击:(115)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11    王建立  Tags:Spring Boot   点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  互联网架构小马哥    Tags:Spring Boot   点击:(115)  评论:(0)  加入收藏
站内最新
站内热门
站内头条