HashMap在Go与Java的底层实现与区别

在Java中

在Java中hash表的底层数据结构与扩容等已经是面试集合类问题中几乎必问的点了。网上有对源码的解析已经非常详细了我们这里还是说说其底层实现。

基础架构

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    private static final long serialVersionUID = 362498820763181265L;
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于等于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当桶(bucket)上的结点数小于等于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table;
    // 一个包含了映射中所有键值对的集合视图
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;
    // 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容
    int threshold;
    // 负载因子
    final float loadFactor;
}

本文所有Java代码均来自JavaGuide(HashMap 源码分析 | JavaGuide),这里主要就是定义一些必要的常量,被用于哈希表的创建参数,扩容参数等待。

然后就是hash表中的Node节点的数据结构,我们的k-v键值对就存储在一个Node类里面。在jdk1.7前其实与redis中的字典Dictionary数据结构中的hash表十分类似,即采用线性搜索和拉链法。在jdk1.8 及以后版本1中,添加了树化,即当节点数大于8就会将当前节点转化为红黑树,这样做的目的主要是为了增加搜索效率,红黑树的时间复杂度为O(log n)如果没有树化链表查询的时间复杂度为O(n) 。接下来就看看JavaGuide中给出的节点类:

链表节点:

// 继承自 Map.Entry<K,V>
static class Node<K,V> implements Map.Entry<K,V> {
       final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
       final K key;//键
       V value;//值
       // 指向下一个节点
       Node<K,V> next;
       Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        // 重写hashCode()方法
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        // 重写 equals() 方法
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
}

树节点:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父
        TreeNode<K,V> left;    // 左
        TreeNode<K,V> right;   // 右
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;           // 判断颜色
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        // 返回根节点
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
       }

resize()

然后我们来重点讲一讲resize()扩容这个方法。

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;
        }
        // 没超过最大值,就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 创建对象时初始化容量大小放在threshold中,此时只需要将其作为新的数组容量
        newCap = oldThr;
    else {
        // signifies using defaults 无参构造函数创建的对象在这里计算容量和阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // 创建时指定了初始化容量或者负载因子,在这里进行阈值初始化,
    	// 或者扩容前的旧容量小于16,在这里计算新的resize上限
        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) {
        // 把每个bucket都移动到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    // 只有一个节点,直接计算元素新的位置即可
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 将红黑树拆分成2棵子树,如果子树节点数小于等于 UNTREEIFY_THRESHOLD(默认为 6),则将子树转换为链表。
                    // 如果子树节点数大于 UNTREEIFY_THRESHOLD,则保持子树的树结构。
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                    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) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

在Java的hashmap中,在jdk1.8以后是通过负载因子的判断来选择是否进行resize()方法默认负载因子是0.75。如果存储数据与当前容量之比为0.75就会进行扩容。同时当我们存储数据过多时,无论我们的hash算法做了什么样的优化,一定还是会有hash冲突的存在,所以为了解决冲突,我们使用拉链法。在插入数据时,我们采用的方式是将已存在hashmap中的键值对的值与插入的值进行比较,如果二者相等就进行覆盖,如果二者不相等就使用尾加法加载链表尾部(在redis5中,使用的是equals()进行比较,因为redis存储的所有值都是String字符串。)。我们将一个拉链称之为一个哈希桶。但是我们试想如果一个桶的节点过于多了,那么我们查找时遍历起来是很花费时间的,在hashtable中使用的是线性搜索法加拉链法来解决这个问题的,但是如果我们一直盲目使用线性搜索法的话,(我们暂且将线性搜索法占据的hash表数组槽位与hash表当前容量的比值称为线性搜索负载因子)当线性搜索负载因子过大时,我们的hash表的查找效率会受到极大的影响。所有在jdk1.8后的树化则很好解决了这个问题,即当拉链上的节点树大于8时,会先对数组容量进行判断,如果小于64先扩容(hashmap扩容都为2倍扩容),否则进行拉链法。(为什么先扩容呢?因为hashmap的hash函数计算与容量有关所以扩容后会得到新的hash值,避免了hash冲突,相较于红黑树的遍历,我们肯定更优先考虑的是这种做法)

在Golang中

基础架构

type hmap struct {
    count int
    flags uint8
    B uint8
    noverflow uint16
    hash0 uint32
    buckets unsafe.Pointer
    oldbuckets unsafe.Pointer
    nevacuate uintptr
    
    extra *mapextra
}


type mapextra struct {
    overflow *[]*bmap
    oldoverflow *[]*bmap
    nextOverflow *bmap
}
  • count 表示当前hash表中的元素。

  • B 表示当前hash表持有的 buckets (即桶数组)数量,由于hash表中桶数量都为2的倍数,所以该字段会存储对数。(这里和redis的hash表一样,在redis的rehash过程中,需要先创建一个2倍旧数组长度的新数组,然后进行hash桶迁移)。

  • oldbuckets是哈希表在扩容时用于保存之前buckets的字段,它的大小是当前buckets的一半。

在go的hashmap中,我们使用溢出桶来降低扩容频率,本质上就是预先分配几个数组空间用于存储超出容量的k-v

扩容方法

  • 开放寻址法

    • 其实就是线性搜索法。简而言之就是依次探测和比较数组中的元素以判断目标键值对是否存在于哈希表,当我们向当前哈希表写入新数据时,如果发生了冲突,就会将键值对写入下一个索引不为空的位置。开放寻址法中对性能影响最大的是装载因子,它是数组中元素数量与数组大小的比值。随着装 载因子的增加,线性探测的平均用时会逐渐增加,这会影响哈希表的读写性能。当装载率超过70% 之后,哈希表的性能就会急剧下降,而一旦装载率达到100%,整个哈希表就会完全失效,这时查找 和插入任意元素的时间复杂度都是O(n),我们需要遍历数组中的全部元素,所以在实现哈希表时一 定要关注装载因子的变化。

  • 拉链法

    • 和jdk 1.7 一样,就不做过多解释。

在go中的k-v添加我们需要注意的是当插入的k-v小于25时会以如下方式插入:

hash := make(map[string]int, 3)
hash["1"] = 1
hash["2"] = 2
hash["3"] = 3

如若大于25个,就会分别创建两个数组,分别存储k 和 v,然后以遍历形式插入。

言归正传,在go的hashmap中扩容条件为:装在因子超过6.5 || 哈希表使用太多溢出桶。

同时由于在go的hashmap的扩容不是原子性的所以需要判断以避免二次扩容(这和redis也一样,增删改查需要判断当前数据库的hash表是否在进行rehash)。

扩容数据结构

说了这么多,接下来我们就来重点介绍go的hashmap扩容的数据结构变化。runtime. evacuate会将一个旧桶中的数据分流到两个新桶,所以它会创建两个用于保存分配 上下文的runtime.evacDst结构体,这两个结构体分别指向了一个新桶。

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
    b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
    newbit := h.noldbuckets()
    if !evacuated(b) {
    var xy [2]evacDst
    x := &xy[0]
    x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
    x.k = add(unsafe.Pointer(x.b), dataOffset)
    x. v = add(x.k, bucketCnt*uintptr(t.keysize))
    y := &xy[1J
    y. b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
    y.k = add(unsafe.Pointer(y.b), dataOffset)
    y.v = add(y.k, bucketCnt*uintptr(t.keysize))
}

go中的hashamp扩容分为等量扩容和翻倍扩容,如果是前者就只初始化一个桶,如果是翻倍扩容,就会初始化两个桶。会把一个链表数据分到新表两个位置,将8个节点分流到两个桶中(这里获取桶位置采用取模或者位运算来得到数据存储的桶位置),然后将k的指针指向两个桶位置。

在数据查询时,会先判断是否在进行分流,如果在进行,就先会从旧桶中读取数据。相较于Java的hashmap它不会一次性将所有的元素重新哈希,而是在每次插入元素时,都会将一部分元素移动到新的桶中。这样可以避免一次性的大量计算,但可能会导致一段时间内的查询效率稍低。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/654208.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JAVA云HIS医院系统源码 云HIS运维平台源码 融合B/S版电子病历系统,支持电子病历四级,saas模式

JAVA云HIS医院系统源码 云HIS运维平台源码 融合B/S版电子病历系统&#xff0c;支持电子病历四级&#xff0c;saas模式 HIS系统就是医院信息管理系统&#xff0c;HIS系统是整个医院信息化的核心&#xff0c;门诊、住院、药房、药库等都是由HIS系统来承载起来的&#xff0c;所以…

高动态范围成像(HDRI)技术在AI去衣中的革新作用

引言&#xff1a; 在计算机视觉和图像处理领域&#xff0c;人工智能&#xff08;AI&#xff09;去衣技术是一项颇具争议但又不容忽视的技术。它不仅在娱乐和多媒体制作领域中扮演着重要角色&#xff0c;还在时尚设计与电子商务中展现了其独特的价值。随着技术的不断进步&#x…

动态规划part02 Day42

LC62不同路径 LC63不同路径II(超时10min) 超时原因分析&#xff1a;思路想错了&#xff0c;即便是正确思路初始化也有点问题&#xff0c;应该将不必要的判断逻辑引入初始化的过程中初始化&#xff1a; 从左上角到[i][0]和[0][j]都只有一条路径dp[i][0]1和dp[0][j]1引入故障&am…

探索减轻 AI 说服伤害的机制方法

随着生成式人工智能&#xff08;AI&#xff09;系统在各个领域的广泛应用&#xff0c;其说服能力也日益增强&#xff0c;引发了对 AI 说服可能带来伤害的担忧。AI 说服的伤害不仅来源于说服的结果&#xff0c;还包括说服过程中可能对个体或社会造成的不利影响。为了系统性地研究…

信息抽取模型TPLinker

1.motivation 早期传统方法首先抽取实体再抽取它们之间的关系&#xff0c;但是忽略了两个任务之间的关联。而后期采取的联合模型都存在着一个严重问题&#xff1a;训练时&#xff0c;真实值作为上下文传入训练&#xff1b;推理时&#xff0c;模型自身生成的值作为上下文传入&a…

DolphinScheduler 3.3.0版本更新一览

Apache DolphinScheduler即将迎来3.3.0版本的发布&#xff0c;届时将有一系列重要的更新和改进。在近期的社区5月份用户线上分享会上&#xff0c;项目PMC 阮文俊为大家介绍了3.3.0版本将带来的主要更新和改进&#xff0c;并为大家指出了如何参与社区的方式。 什么是DolphinSch…

企业内网终端监控管理软件有哪些?推荐4款企业终端监控管理软件

企业内网终端监控管理软件是一种专为企业内部网络设计的安全与管理工具&#xff0c;旨在帮助企业管理、监控和保护其内部网络中的各种终端设备&#xff0c;如个人电脑、笔记本、移动设备等。 这类软件的主要功能包括但不限于以下几个方面&#xff1a; 1&#xff0c;实时监控&a…

Java面试八股之start()和run()的区别

start()和run()的区别 在Java中&#xff0c;run()方法和start()方法是与线程操作紧密相关的&#xff0c;两者之间存在本质的区别&#xff1a; start()是Thread类的一个实例方法&#xff0c;它的主要作用是启动一个新的线程。当调用线程对象的start()方法时&#xff0c;Java虚…

手搓顺序表(C语言)

目录 SeqList.h SeqList.c 头插尾插复用任意位置插入 头删尾删复用任意位置删除 SLtest.c 测试示例 顺序表优劣分析 SeqList.h //SeqList.h#pragma once#include <stdio.h> #include <assert.h> #include <stdlib.h> #define IN_CY 3typedef int S…

Android环境下Mesa初始化流程重学习之eglInitialize

Mesa初始化流程重学习之eglInitialize 引言 说来也惭愧&#xff0c;Mesa搞了这么久了&#xff0c;每次都想深入下&#xff0c;可是每次都是浅尝辄止了。这次趁着有了一定的闲暇时间并且有了调试景嘉微显卡的机会&#xff0c;还是想重新学习下&#xff0c;深入研究下&#xff0…

MongoDB分片集群容灾方案

MongoDB分片集群容灾方案 1. 集群同步工具介绍1.1 第三方数据同步工具mongoshake1.2 官方同步工具mongosync 2. 工具对比2.1 数据一致性2.2 稳定性和可靠性2.3 维护成本 3. 总结 1. 集群同步工具介绍 最近客户咨询MongoDB分片集群市面上主流的容灾方案&#xff0c;所以抽空整理…

Node.js —— Express中服务器的创建、托管静态资源、nodemon

目录 Express的安装 创建基本的 Web 服务器 监听GET请求 监听POST请求 把内容响应给客户端 ​编辑获取 URL 中携带的查询参数 ​编辑获取 URL 中的动态参数 ​编辑托管静态资源 express.static() 托管多个静态资源目录 挂载路径前缀 nodemon: 为什么要使用 nodemon 安…

如何让UE4.26使用VS2022【Windows,源码下载】

使用UE5一直用的是VS2022&#xff0c;都是因为团队需要&#xff0c;只能用UE4&#xff0c;而我电脑中拥有的UE4的版本是UE4.26以及VS2022&#xff0c;我不可能去下载VS2019来为这么一个项目&#xff0c;所以就研究了一下是哪里阻止了UE4.26不让我使用VS2022. 首先下载UE4.26源码…

守护景区安全:探讨景区视频监控方案的搭建及必要性

据新闻报道&#xff0c;5月25日&#xff0c;安徽黄山景区内发生雷击&#xff0c;闪电击中飞来石景点的护栏&#xff0c;多人被碎石砸中受伤。景区工作人员表示&#xff0c;飞来石附近本就属于雷区&#xff0c;当天曾发过两次雷电预警。 随着旅游业的繁荣发展&#xff0c;越来越…

掌握Adobe XD:为自学者准备的软件学习秘籍

相信了解一些设计软件的朋友都听说过这个软件&#xff0c;Adobe XD软件是一款功能强大的原型创建工具。随着Adobe XD软件越来越受到用户的青睐&#xff0c;它几乎涵盖了所有大中小企业和企业的设计&#xff0c;可以说是设计公司最常用的软件之一。Adobe XD软件可以在很多方面满…

Android制作.9图

需求背景&#xff1a;android 启动图变形 开发语言&#xff1a;uni-app&#xff0c;uni-app官网 俗语曰&#xff1a;授人以鱼不如授人以渔 原创地址&#xff1a;Android制作.9图 语雀 一.工具 使用android studio&#xff0c;因为android studio已经集成.9.png制作工具&a…

godot4.2 + GDextension c++在 vs code 中断点调试配置

游戏开发中如果做不到自己编写的代码做断点调试&#xff0c;无不是瞎子摸象&#xff0c;特别是C这么底层的语言。这2天开始在VS studio中折腾&#xff0c;一直折腾不出结果&#xff0c;几次想要放弃GODOT。最终今天在VS code中搞定了这断点调试C代码。 在上一篇文章我已经做好了…

axios和ts的简单使用

按照官网的使用案例简单记下笔记 1&#xff1a;安装 npm install axios 2&#xff1a;案例 一个简单的config配置信息 // 发起一个post请求 axios({method: post,url: /user/12345,data: {firstName: Fred,lastName: Flintstone} }); case // 在 node.js 用GET请求获取…

基于springboot+vue的公司资产网站(全套)

一、系统架构 前端&#xff1a;vue2 | element-ui 后端&#xff1a;springboot | mybatis 环境&#xff1a;jdk1.8 | mysql | maven | node 二、代码及数据库 三、功能介绍 01. 管理后台-登录 02. 管理后台-首页 03. 管理后台-个人中心-修改密码 04. 管理后台-个人…

蓝桥杯第1022题 玩具蛇 基础DFS C++ Java

题目 思路和解题方法 问题理解&#xff1a;此题要求找出将一条由16节正方形构成的玩具蛇放入4x4的方格中的不同方式数。每节蛇可以是直线或直角转弯&#xff0c;且蛇的形状需要完全覆盖盒子里的16个格子&#xff0c;每个格子仅被蛇的一个部分占据。 状态表示&#xff1a;使用一…