Java Map

news2025/2/6 14:01:08

Map——广义集合的子集

HashTable是早期Java类库提供的一个哈希表实现,扩展了Dictionary类,类结构上与HashMap明显不同,本身是同步的,不支持null键和值,由于同步导致的性能开销,已经很少被推荐使用。

HashMap是应用广泛的哈希表实现,扩展了AbstractMap类,行为上大致上与HashTable一致,主要区别在于HashMap不是同步的,支持null键和值等。HashMap进行put或者get操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选。在Java 8里,HashMap本身发生了非常大的变化。

LinkedHashMap提供的是遍历顺序符合插入顺序,它的实现是通过为键值对维护一个双向链表。通过特定构造函数可以创建反映访问(put、get、compute)顺序的实例。这种行为适用于一些特定场景,比如构建一个空间占用敏感的资源池,希望可以自动将最不常被访问的对象释放掉,就可以利用LinkedHashMap提供的机制来实现。

TreeMap则是基于红黑树的一种提供顺序访问的Map扩展了AbstractMap类,和HashMap不同,它的get、put、remove操作都是O(log(n))的时间复杂度,具体顺序可以由指定的Comparator来决定,或者根据键的自然顺序Comparable来判断。为了避免模棱两可的情况,自然顺序同样需要符合一个约定,就是compareTo的返回值需要和equals一致。当不遵守约定时,两个不符合唯一性(equals)要求的对象被当作是同一个(compareTo返回了0),这会导致歧义的行为表现。

// 用TreeMap的put方法实现举例
public V put(K key, V value) {
    Entry<K,V> t = …
    cmp = k.compareTo(t.key);
    if (cmp < 0)
        t = t.left;
    else if (cmp > 0)
        t = t.right;
    else
        return t.setValue(value);
        // ...
   }

注:

1. HashTable本身比较低效,它的实现就是将put、get、size等方法加上synchronized,导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。

2. 大部分使用Map的场景,通常是放入、访问、删除,对顺序没有特别要求,HashMap在这种情况下基本是最好的选择。HashMap的性能表现非常依赖于哈希码的有效性,hashCode和equals的一些基本约定如下,

  • equals相等,hashCode一定要相等。
  • 重写了hashCode也要重写equals。
  • hashCode需要保持一致性,状态改变返回的哈希值仍然要一致。
  • equals的对称、反射、传递等特性。

HashMap剖析

HashMap内部结构可以看作是数组(Node<K,V>[] table)和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组的寻址;哈希值相同的键值对以链表形式存储。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。

从非拷贝构造函数的实现来看,这个数组似乎并没有在最初就初始化好,仅仅设置了一些初始值。HashMap是按照 lazy-load 原则在首次使用时被初始化(拷贝构造函数除外)。

public HashMap(int initialCapacity, float loadFactor){  
    // ... 
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

put方法里只有一个 putVal 调用,putVal方法本身逻辑非常集中,从初始化、扩容到树化,全都和它有关

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbent,
               boolean evit) {
    Node<K,V>[] tab; Node<K,V> p; int , i;
    if ((tab = table) == null || (n = tab.length) = 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == ull)
        tab[i] = newNode(hash, key, value, nll);
    else {
        // ...
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for first 
           treeifyBin(tab, hash);
        //  ... 
     }
}
  • 如果数组是null,resize方法会负责初始化它,这从tab = resize()可以看出。
  • resize方法有两个职责,创建初始存储数组,或者在容量不满足需求的时候,进行扩容(resize)。
  • 在放置新的键值对的过程中,如果发生下面条件,就会发生扩容。
  • if (++size > threshold)
        resize();
  • 具体键值对在哈希表中的位置(数组index)取决于下面的位运算。哈希值的源头并不是key本身的hashCode,而是HashMap内部的另外一个hash方法。为什么这里需要将高位数据移位到低位进行异或运算呢?因为有些数据计算出的哈希值差异主要在高位,而HashMap里的哈希寻址是忽略容量以上的高位的,这种处理可以有效避免类似情况下的哈希碰撞。
  • i = (n - 1) & hash
    
    // 上面说的hash方法
    static final int hash(Object kye) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16;
    }
  • 前面的链表结构(这里叫bin)会在达到一定阈值时发生树化,为什么HashMap需要对bin进行处理?本质上这是个安全问题。因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,就会形成一个链表,链表查询是线性的,严重影响存取的性能。在现实世界构造哈希冲突的数据并不复杂,恶意代码可以用这些数据大量与服务器端交互,导致服务器端CPU大量占用,这就构成了哈希碰撞拒绝服务攻击,国内一线互联网公司就发生过类似攻击事件。

resize方法,不考虑极端情况(容量理论最大极限由MAXIMUM_CAPACITY指定,数值为 1<<30,2的30次方),可以归纳如下,

  • 阈值等于(负载因子)x(容量),如果构建HashMap时没指定它们,那就用相应的默认常量值。
  • 阈值通常是以倍数进行调整 (newThr = oldThr << 1),根据putVal中的逻辑,当元素个数超过阈值时就调整Map大小。
  • 扩容后,需要将老数组中的元素重新放置到新数组,这是扩容的一个主要开销来源。
final Node<K,V>[] resize() {
    // ...
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACIY &&
                oldCap >= DEFAULT_INITIAL_CAPAITY)
        newThr = oldThr << 1; // double there
       // ... 
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {  
        // zero initial threshold signifies using defaultsfults
        newCap = DEFAULT_INITIAL_CAPAITY;
        newThr = (int)(DEFAULT_LOAD_ATOR* DEFAULT_INITIAL_CAPACITY;
    }
    if (newThr ==0) {
        float ft = (float)newCap * loadFator;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
    }
    threshold = neThr;
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newap];
    table = n;
    // 移动到新的数组结构e数组结构 
   }

容量和负载系数决定了可用桶的数量,空桶太多会浪费空间,如果用的太满会严重影响操作的性能。极端情况下,假设只有一个桶,那么它就退化成了链表,完全不能提供所谓常数时间存的性能。

关于容量选择,如果能知道HashMap要存取的键值对数量,可以预先设置合适的容量大小。具体数值可以根据扩容发生的条件来简单预估,根据前面的分析知道它要符合计算条件,所以预先设置的容量要满足大于“预估元素数量/负载因子”,同时它是2的幂数。

负载因子 * 容量 > 元素数量

关于负载因子,如果没有特别需求,不要轻易进行更改,JDK自身的默认负载因子是非常符合通用场景需求的。如果确实需要调整,建议不要设置超过0.75的数值,因为会显著增加冲突,降低HashMap的性能。如果使用太小的负载因子,按照上面的公式,预设容量值也进行调整,否则可能会导致更加频繁的扩容,增加无谓的开销,本身访问性能也会受影响。

树化逻辑主要在 treeifyBin 方法中,当 bin 的数量大于 TREEIFY_THRESHOLD 时,

  • 如果容量小于 MIN_TREEIFY_CAPACITY,只会进行简单地扩容。
  • 如果容量大于 MIN_TREEIFY_CAPACITY ,则会进行树化改造。
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //树化改造逻辑
    }
}

同步的包装容器(Collections.synchronizedMap)

HashMap不是线程安全的,并发情况会导致类似CPU占用100%等一些问题,Collections提供的同步包装器利用输入Map构造了另一个同步版本,所有操作虽然不再声明为synchronized方法,但是还是利用了this作为互斥的mutex,没有真正意义上的改进。HashTable或者同步包装版本都用的是粗粒度的同步方式,只适合非高并发场景。

private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize
    // …
    public int size() {
        synchronized (mutex) {return m.size();}
    }
 // … 
}

思考个问题,高并发下Map场景该怎么处理呢?

Have Fun

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL — 数据库基础】MySQL的安装与配置 & 数据库简单介绍

数据库基础 本节目标 掌握关系型数据库&#xff0c;数据库的作用掌握在Windows和Linux系统下安装MySQL数据库了解客户端工具的基本使用和SQL分类了解MySQL架构和存储引擎 1. 数据库的安装与配置 1.1 确认MYSQL版本 处理无法在 cmd 中使用 mysql 命令的情况&a…

实测数据处理(BP算法处理)——SAR成像算法系列(九)

系列文章目录 《SAR学习笔记-SAR成像算法系列&#xff08;一&#xff09;》 《后向投影算法&#xff08;BPA&#xff09;-SAR成像算法系列&#xff08;二&#xff09;》 《后向投影算法&#xff08;续&#xff09;-SAR成像算法系列&#xff08;八&#xff09;》 文章目录 一…

(数据结构与算法)如何提高学习算法的效率?面试算法重点有哪些?面试需要哪些能力?

面试官眼中的求职者 通过对你算法的考察&#xff01;&#xff01;&#xff01;&#xff01; 缩进太多&#xff01;&#xff01;一般不要超过三层&#xff01;&#xff01;&#xff01;缩进越少&#xff0c;bug越少&#xff1b;逻辑比较复杂&#xff0c;把这些包装成为函数&…

Day05:缓存双写一致性

redis做为缓存&#xff0c;mysql的数据如何与redis进行同步呢&#xff1f;&#xff08;双写一致性–强一致&#xff09; 一种是一致性要求比较高的同步方案&#xff0c;另一种是允许延迟一致的异步通知。 什么是双写一致性&#xff1f; 双写一致性&#xff1a;当修改了数据库…

vue3+typescript自定义input组件

官方文档&#xff1a;https://cn.vuejs.org/guide/components/events#%E5%AE%9A%E4%B9%89%E8%87%AA%E5%AE%9A%E4%B9%89%E4%BA%8B%E4%BB%B6 触发与监听事件​ 在组件的模板表达式中&#xff0c;可以直接使用 $emit 方法触发自定义事件 (例如&#xff1a;在 v-on 的处理函数中)…

代码之丑第一期-缩进

各位小伙伴们,大家好!咱今天就算是正式开张了。实不相瞒,第一期的内容早已写好,但唯独这开篇方式,笔者想了好些时间,包括但不限于如下风格: 斗破苍穹式(已经三刷):代码优雅之力,三段!级别:低级!百年孤独式(困扰于错综复杂的人物关系,放弃):多年以后,面对吐槽…

idea2024加载flowable6.8.1.36遇到的问题-idea启动flowable问题flowable源码启动问题

代码下载地址&#xff1a; https://gitee.com/hanpenghu_admin_admin/flowable6.8.1.git 1.首先是通过顶层目录maven clean install 发现很多子模块并不会install本地mavenStore库&#xff0c;这导致了&#xff0c;一堆相互依赖的模块报错找不到&#xff0c;所以需要根据报错…

Vue.js 中 v-for 指令的三种常见用法详解及key、value、id的作用

作者&#xff1a;CSDN-PleaSure乐事 欢迎大家阅读我的博客 希望大家喜欢 使用环境&#xff1a;WebStorm 目录 遍历数组 介绍 代码 遍历对象数组 介绍 代码 遍历对象本身 介绍 代码 效果呈现 key、value、id的作用 1. value 2. key 3. id 在 Vue.js 中&#xff0c…

【论文投稿】国产游戏技术:迈向全球引领者的征途

【IEEE出版南方科技大学】第十一届电气工程与自动化国际会议&#xff08;IFEEA 2024)_艾思科蓝_学术一站式服务平台 更多学术会议论文投稿请看&#xff1a;https://ais.cn/u/nuyAF3 目录 国产游戏技术能否引领全球&#xff1f; 一、国产游戏技术的崛起之路 1.1 初期探索与积…

React的ts文件中通过createElement拼接一段内容出来

比如接口返回一个值 const values [23.00, 40.00/kg];想做到如下效果&#xff0c; 如果单纯的用render渲染会很简单&#xff0c; 但是在ts文件中处理&#xff0c;所以采用了createElement拼接 代码如下&#xff1a; format: (values: string[]) > {if (!values || !val…

江协科技最新OLED保姆级移植hal库

江协科技最新OLED移植到hal库保姆级步骤 源码工程存档 工程和源码下载(密码 1i8y) 原因 江协科技的开源OLED封装的非常完美, 可以满足我们日常的大部分开发, 如果可以用在hal库 ,将是如虎添翼, 为我们开发调试又增加一个新的瑞士军刀, 所以我们接下来手把手的去官网移植源码…

HarmonyOS:使用Emitter进行线程间通信

Emitter主要提供线程间发送和处理事件的能力&#xff0c;包括对持续订阅事件或单次订阅事件的处理、取消订阅事件、发送事件到事件队列等。 一、Emitter的开发步骤如下&#xff1a; 订阅事件 import { emitter } from kit.BasicServicesKit; import { promptAction } from kit.…

Wi-Fi 进化论:从过去到未来(6/10)

Wi-Fi&#xff08;发音&#xff1a; /ˈwaɪfaɪ/&#xff09;&#xff0c;在中文里又称作“移动热点”&#xff0c;是Wi-Fi联盟制造商的商标作为产品的品牌认证&#xff0c;是基于IEEE 802.11标准的无线局域网通信技术 [6]。基于两套系统的密切相关&#xff0c;也常有人把Wi-F…

【C++初阶】第5课—动态内存管理

文章目录 1. 内存分布2. C语言动态内存管理3. C内存管理方式3.1 new/delete操作内置类型3.2 new和delete操作自定义类型 4. operator new和operator delete函数5. new和delete的实现原理6. malloc/free和new/delete的区别7. 定位new表达式(了解即可) 1. 内存分布 先来做一个关于…

学习threejs,设置envMap环境贴图创建反光效果

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.CubeTextureLoader 立…

Matlab Simulink HDL Coder FPGA开发初体验—计数器

目录 一、Simulink设计及仿真二、Verilog HDL代码转换1、参数配置2、HDL代码生成 三、ModelSim仿真分析1、使用自己编写的Testbench文件进行仿真2、使用HDL Coder生成的Testbench文件进行仿真 前言 Simulink HDL Coder‌是一款将Simulink和Stateflow模型转化为可综合的Verilog和…

【C语言】扫雷游戏(一)

我们先设计一个简单的9*9棋盘并有10个雷的扫雷游戏。 1&#xff0c;可以用数组存放&#xff0c;如果有雷就用1表示&#xff0c;没雷就用0表示。 2&#xff0c;排查(2,5)这个坐标时&#xff0c;我们访问周围的⼀圈8个位置黄色统计周围雷的个数是1。排查(8,6)这个坐标时&#xf…

实现点名神器的pyqt6实现

利用python gui创建点名神器&#xff0c;包含加分、导出加分记录、清除加分记录。 点名页面 首先导入学生信息 导入成功 开始点名 点击停止 点过之后&#xff0c;点击加分 加完分 查看加分记录 可以直接进入导出记录和清除历史。 此文到此结束&#xff0c;想要源码的请私聊我&a…

【UE5 C++】判断两点连线是否穿过球体

目录 前言 原理 代码 测试 结果 前言 通过数学原理判断空间中任意两点的连线是否穿过球体&#xff0c;再通过射线检测检验算法的正确性。 原理 &#xff08;1&#xff09;设球体球心的坐标为 &#xff0c;半径为r&#xff1b; &#xff08;2&#xff09;设线段中A点的坐…

【AI】数据,算力,算法和应用(3)

三、算法 算法这个词&#xff0c;我们都不陌生。 从接触计算机&#xff0c;就知道有“算法”这样一个神秘的名词存在。象征着专业、权威、神秘、高难等等。 算法是一组有序的解决问题的规则和指令&#xff0c;用于解决特定问题的一系列步骤。算法可以被看作是解决问题的方法…