HashMap常用方法及底层原理

目录

    • 一、什么是HashMap
    • 二、HashMap的链表与红黑树
      • 1、数据结构
      • 2、链表转为红黑树
      • 3、红黑树退化为链表
    • 三、存储(put)操作
    • 四、读取(get)操作
    • 五、扩容(resize)操作
    • 六、HashMap的线程安全与顺序
      • 1、线程安全
      • 2、有序的Map

一、什么是HashMap

    HashMap 是 Java 中的一个关键数据结构,属于 java.util 包。它基于哈希表的实现,提供了快速查找、插入和删除操作。以下是 HashMap 的一些主要特性:

  1. 键值对存储:HashMap 存储键值对(key-value pairs),其中键(key)是唯一的。

  2. 非同步:HashMap 不是线程安全的。在多线程环境中,如果多个线程同时修改 HashMap,而没有适当的同步措施,可能会导致不可预知的行为。

  3. 允许空键和空值:HashMap 允许键或值为 null。

  4. 非有序:HashMap 不保证元素的顺序,特别是它不保证该顺序恒久不变。

  5. 初始容量和负载因子:可以通过构造函数设置 HashMap 的初始容量和负载因子。初始容量是哈希表中桶的数量,负载因子是一个影响哈希表性能的参数,它定义了哈希表在其容量自动增加之前可以达到多满。

  6. 哈希冲突解决:当两个对象具有相同的哈希码时,会发生哈希冲突。HashMap 使用链表(在 Java 8 之前)或链表和红黑树(Java 8 及之后)来解决冲突。

  7. 性能:HashMap 提供了常数时间的性能(即 O(1) 时间复杂度)对于 get 和 put 操作,假设哈希函数良好且哈希表没有过载。

  8. 迭代器:HashMap 提供了键集(key set)、值集(values)和键值对集(entry set)的视图,它们都可以被迭代。

  9. 默认构造方法:如果使用无参构造方法创建 HashMap,它会使用默认的初始容量(16)和默认的负载因子(0.75)。

主要特性总结key-value形式的键值对;无序不重复;线程不安全

    

二、HashMap的链表与红黑树

1、数据结构

HashMap是一种存取高效但不保证有序的集合,它的数据结构在1.7里面是 数组+链表在1.8之后是数组+链表+红黑树

在这里插入图片描述
    
默认初始容量16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

将链表转为红黑树的阈值

 static final int TREEIFY_THRESHOLD = 8;

将红黑树转化为链表的阈值

 static final int UNTREEIFY_THRESHOLD = 6;

将链表转化为红黑树时,数组的大小必须大于等于这个值。否则如果 TREEIFY_THRESHOLD 大于8,将扩容,而不是转为红黑树

static final int MIN_TREEIFY_CAPACITY = 64;

    

2、链表转为红黑树

在 Java 8 及以后的版本中,HashMap 在某些情况下会将链表转换为红黑树,以提高搜索效率。这种转换发生在以下两个条件同时满足时:

  1. 树化阈值:HashMap 有一个树化阈值(treeify threshold),当链表的长度超过这个值时,链表会被转换为红黑树。在 Java 8 中,这个值默认是 8。

  2. 桶(bucket)的数量:如果桶的数量小于 64,即使链表长度达到 8,也不会立即转换为红黑树。这是为了避免在哈希表较小时过度优化,因为小规模的哈希表中,链表的性能已经足够好。

    
转换过程
    当 HashMap 进行扩容时,如果桶的数量增加到 64 或更多,那么在重新计算哈希值并重新插入元素的过程中,任何长度大于等于 8 的链表都会被转换为红黑树。这个转换是在扩容过程中自动完成的。

    
为什么有这个限制

  • 性能考虑:在哈希表较小时,链表的性能通常已经足够好,不需要额外的复杂性来维护红黑树。
  • 内存考虑:红黑树比链表占用更多的内存,因此在小规模的哈希表中使用链表可以节省内存。

    在实际应用中,这意味着如果你的 HashMap 初始容量设置得较小,或者在插入数据时没有触发扩容,那么即使某些链表的长度超过了 8,它们也可能不会立即转换为红黑树。只有当哈希表的容量增加到一定大小,且链表长度满足条件时,才会进行转换
    

3、红黑树退化为链表

     当一个桶中的红黑树的节点数量减少到一定阈值以下时,HashMap 会将这个红黑树转换回链表。这个阈值默认是 6。这意味着,如果一个桶中的红黑树的节点数量降到 6 个或更少,那么这个红黑树会被转换回链表。
    
为什么需要转换

  • 性能优化:链表在节点数量较少时,其操作(如搜索、插入、删除)的性能通常优于红黑树,因为链表的结构更简单,没有红黑树的复杂性。
  • 内存使用:红黑树比链表占用更多的内存,因为它需要存储额外的指针(用于维护树的结构)。当节点数量较少时,使用链表可以减少内存的使用。

    
转换过程

转换过程通常发生在以下情况下:

 1. 删除操作:当从 HashMap 中删除元素时,如果某个桶中的红黑树节点数量减少到 6 个或更少,这个红黑树会被转换回链表。2. 扩容操作:在 HashMap 进行扩容时,如果桶的数量增加,那么在重新计算哈希值并重新插入元素的过程中,如果某个桶中的红黑树节点数量减少到 6 个或更少,这个红黑树会被转换回链表。

    

三、存储(put)操作

    
    jdk1.7采用的是头插法 ,因头插法在扩容时导致的死循环,jdk1.8中链表插入采用的是尾插法。

put操作的步骤如下:

  1. 待存储的key进行hash计算
  2. 如果hash数组为空或者数组长度为0,则进行初始化
  3. 数组根据key计算后的hash值进行索引数组元素:
    • 如果索引出来的元素为空,则新创建一个Node节点并添加到数组中

    • 如果索引出来的元素不为空,则比较索引出来元素的key与传入的key进行"=="或equals的比较

      ① 如果一致则用新的value值替换旧的value值
      ②如果不一致就判断索引出来节点是不是树形节点,如果是则按照树形节点进行更新
      ③如果不是树形节点,则判断索引出来的节点的next节点是否为空,如果下一个节点为空,则新建下一个节点,此时需要判断链表的长度是否大于等于8,如果等于8,则要进行树化转为红黑树。
      ④ 如果下一个节点不为空,就一直循环找到节点的hash值与传入key的hash值一致,key通过"=="或equals比较是一致的。然后用新的value值替换旧的value值

   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 onlyIfAbsent, boolean evict) {Node<K,V>[] tab;Node<K,V> p;int n, i;//如果table为空,则初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//key对应的桶不存在,则创建一个新节点并添加到桶中if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);//key对应的桶存在,则遍历桶,找到key对应的节点,如果key相同,则更新value,否则创建一个新节点else {Node<K,V> e; K k;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;// 判断该节点是不是树形节点else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//节点key不相同也不是树形节点else {for (int binCount = 0; ; ++binCount) {//节点的next节点为空,则新建节点并判断是否需要树化if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 节点的next节点不为空,且key相同if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

    

四、读取(get)操作

    
获取数据步骤如下:

  1. 如果数组不为空,则根据hash值找到对应的桶,对应的桶为空,就返回null
  2. 对应的桶不为空,如果桶的第一个元素的key和key相等,则返回该元素
  3. 如果桶的第一个元素的key和key不相等
    • 如果桶的第一个元素的next节点不是树节点,则遍历桶,找到key对应的节点,如果key相同,则返回该节点
    • 如果桶的第一个元素是树节点,则调用树节点的getTreeNode方法
 public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab;Node<K,V> first, e; int n; K k;// 如果数组不为空,则根据hash值找到对应的桶,对应的桶也不为空if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {// 如果桶的第一个元素的key和key相等,则返回该元素if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 如果桶的第一个元素的next节点不是树节点,则遍历桶,找到key对应的节点,如果key相同,则返回该节点if ((e = first.next) != null) {// 如果桶的第一个元素是树节点,则调用树节点的getTreeNode方法if (first instanceof HashMap.TreeNode)return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;
}

    

五、扩容(resize)操作

    
函数resize()用于实现哈希表的扩容操作。具体步骤如下:

  1. 获取旧表信息

    • 获取当前哈希表table的引用。
    • 获取当前哈希表的容量oldCap。
    • 获取当前哈希表的阈值oldThr。

        

  2. 判断是否需要扩容:

    • 如果当前容量大于等于最大容量MAXIMUM_CAPACITY,设置阈值为整型最大值并返回旧表。
    • 如果当前容量大于0且小于最大容量,计算新容量newCap为旧容量的两倍,并更新阈值newThr为旧阈值的两倍。
    • 如果当前容量为0但阈值大于0,将新容量设为阈值。
    • 如果当前容量和阈值都为0,使用默认初始容量DEFAULT_INITIAL_CAPACITY并计算阈值。
          
  3. 计算新阈值

    • 如果新阈值仍为0,根据负载因子计算新阈值。
          
  4. 创建新表

    • 更新阈值threshold为newThr。
    • 创建新的哈希表newTab,大小为newCap。
          
  5. 迁移旧表数据:

    • 遍历旧表oldTab中的每个桶。
    • 对于非空的桶,将其元素迁移到新表中。
    • 如果桶中的元素只有一个,直接插入新表对应位置。
    • 如果桶中的元素是红黑树节点,调用split方法进行拆分。
    • 如果桶中的元素是链表,遍历链表并根据哈希值分成两部分,分别插入新表的不同位置。
 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倍且小于最大容量且旧的容量大于等于默认容量,新阈值设置为旧的容量的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold 新的阈值为旧的阈值的2倍}// 如果当前容量为0但阈值大于0,将阈值设为新容量。else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;// 如果当前容量和阈值都为0,使用默认初始容量DEFAULT_INITIAL_CAPACITY并计算阈值。else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//  如果新阈值仍为0,根据负载因子计算新阈值。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"})//创建新的哈希表newTab,大小为newCap。Node<K,V>[] newTab = (Node<K,V>[])new HashMap.Node[newCap];table = newTab;if (oldTab != null) {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 HashMap.TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}

    

六、HashMap的线程安全与顺序

1、线程安全

HashMap 是非线程安全的,这意味着多个线程同时修改 HashMap 可能会导致不可预知的行为。如果需要在多线程环境中使用 HashMap,有几种方法可以使其线程安全:

  1. 使用 Collections.synchronizedMap 方法

    Java 提供了一个 Collections.synchronizedMap 方法,可以将 HashMap 包装为线程安全的 Map。

    Map<K, V> map = Collections.synchronizedMap(new HashMap<K, V>());
    
  2. 使用 ConcurrentHashMap

    ConcurrentHashMap 是 HashMap 的线程安全版本,它提供了更好的并发性能。通常,它是实现线程安全的首选方式,因为它允许多个线程同时读写而不需要外部同步。

    Map<K, V> map = new ConcurrentHashMap<K, V>();
    
  3. 使用 synchronized 块或方法

    在访问 HashMap 的方法上使用 synchronized 关键字,确保在修改 HashMap 时只有一个线程可以执行。

    public void put(K key, V value) {synchronized (this) {this.map.put(key, value);}
    }
    

        

2、有序的Map

     HashMap 本身不保证有序,即它不保证元素的顺序,无论是插入顺序还是自然顺序。如果需要一个有序的映射,你可以使用以下几种替代方案:

  1. LinkedHashMap:

    LinkedHashMap 是 HashMap 的一个子类,它维护了元素的插入顺序或者访问顺序。如果你想要元素按照插入顺序存储,可以使用 LinkedHashMap

  2. TreeMap:

    TreeMap 是一个基于红黑树实现的 NavigableMap 接口的类,它可以按照元素的自然顺序或者自定义比较器(Comparator)定义的顺序来存储元素。TreeMap 保证了元素的有序性,并且提供了一些导航方法,如 firstEntry()、lastEntry()、higherEntry() 等。

    Map<K, V> map = new TreeMap<K, V>();
    

    或者,如果你想要自定义排序,可以提供一个比较器:

    Comparator<K> comparator = ...;
    Map<K, V> map = new TreeMap<K, V>(comparator);
    
  3. 自定义有序 HashMap:

    如果你需要 HashMap 的某些特性,并且想要保持顺序,你可以自己实现一个有序的 HashMap。这通常涉及到在内部维护一个列表来跟踪插入顺序。
        

  4. Hashtable:

    虽然 Hashtable 是一个遗留类,但它是同步的,并且它按照插入顺序维护键的顺序。然而,由于 Hashtable 的性能通常不如 HashMap 和 LinkedHashMap,并且它不是线程安全的,所以通常不推荐使用。

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

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

相关文章

【LeetCode每日一题】2024年9月第二周(上)

2024.9.9 中等 难度评分 1333 链接&#xff1a;2181. 合并零之间的节点 &#xff08;1&#xff09;题目描述&#xff1a; &#xff08;2&#xff09;示例 &#xff08;3&#xff09;分析 整体来说&#xff0c;描述还算清晰的题目&#xff0c;找到0节点所框定的区域&#xff0c…

Pandas读取某列、某行数据——loc、iloc区别

loc&#xff1a;通过行、列的名称或标签来索引 iloc&#xff1a;通过行、列的索引位置来寻找数据 首先&#xff0c;我们先创建一个DataFrame生成数据 import pandas as pddata {a:[1,2,3,4,5],b:[6,7,8,9,10],c:[11,12,13,14,15] } data pd.DataFrame(data) print(data) 运行…

工具、环境等其他小问题归纳

此篇文章内容会不定期更新&#xff0c;仅作为学习过程中的笔记记录 一、查询Windows 10环境下python版本与安装路径 若电脑成功安装了python环境&#xff0c;不小心忘了版本。 I、查询版本 1、cmd窗口快捷查询 Win R 输入cmd 进入窗口&#xff1b; 直接输入 python --version …

[数据集][目标检测]血细胞检测数据集VOC+YOLO格式2757张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2757 标注数量(xml文件个数)&#xff1a;2757 标注数量(txt文件个数)&#xff1a;2757 标注…

关于武汉芯景科技有限公司的IIC电平转换芯片XJ9517开发指南(兼容PCF9517)

一、芯片引脚介绍 1.芯片引脚 2.引脚描述 二、系统结构图 三、功能描述 1.电平转换 2.芯片使能/失能 EN 引脚为高电平有效&#xff0c;内部上拉至 VCC&#xff08;B&#xff09;&#xff0c;允许用户选择中继器何时有效。这可用于在上电时隔离行为不良的从机&#xff0c;直到…

4052A/4052B/4052C/4052D/4052E/4052F/4052G /4052H信号/频谱分析仪

4052A/4052B/4052C/4052D/4052E/4052F/4052G /4052H信号/频谱分析仪 苏州新利通 Ceyear 4052具备出色的测试动态范围、相位噪声、幅度精度和测试速度&#xff0c;具备频谱分析、I/Q分析、实时频谱分析、瞬态分析、矢量信号分析、脉冲分析、音频分析等丰富的测试功能。 Ceyear…

OpenAI发布o1预览模型:推理能力更强可达理科博士生水准

近日OpenAI宣布推出了新一代 AI 模型系列 OpenAI o1&#xff0c;按照官方技术博客说法&#xff0c;o1 在推理能力上代表了人工智能最强的水平。 那究竟是怎么一回事呢&#xff1f; OpenAI CEO Sam Altman 表示&#xff1a;o1 系列的推出代表了 AI 能力的新起点&#xff0c;能…

240909-ChuanhuChatGPT集成Ollama的环境配置

A. 最终效果 B. 需求文件 requirements.txt (至少需要安装这个&#xff0c;具体参见官网)requirements_advanced.txt &#xff08;如果安装了Ollama&#xff0c;并且可以进行对话&#xff0c;可以不需要安装&#xff0c;具体参见官网&#xff09;requirements_succcess.txt&am…

gin配置swagger文档

一、基本准备工作 1、安装依赖包 go get -u github.com/swaggo/swag/cmd/swag go get -u github.com/swaggo/gin-swagger go get -u github.com/swaggo/files2、在根目录上配置swagger的路由文件 //2.初始化路由router : initialize.Routers()// 配置swaggerdocs.SwaggerInfo…

微服务杂谈

几个概念 还是第一次听说Spring Cloud Alibaba &#xff0c;真是孤陋寡闻了&#xff0c;以前只知道 SpringCloud 是为了搭建微服务的&#xff0c;spring boot 则是快速创建一个项目&#xff0c;也可以是一个微服务 。那么SpringCloud 和 Spring boot 有什么区别呢&#xff1f;S…

Unity for Android使用蓝牙低功耗Bluetooth LE

Unity2021.3.35f1 插件&#xff1a;Bluetooth LE for iOS and Android v2.3.unitypackage 1、将插件资源包导入unity中 2.修改插件中的AndroidManifest文件 <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schem…

系统优化工具 | PC Cleaner v9.7.0.3 绿色版

PC Cleaner是一款功能强大的电脑清理和优化工具&#xff0c;旨在通过清理系统垃圾文件、解除恶意软件和优化系统性能来提高计算机的运行效率。该软件提供了多种功能&#xff0c;可以帮助用户维护和提升计算机的整体表现。 PC Cleaner 支持 Windows 7 及以上操作系统&#xff0…

Qt使用绿色pdf阅读器打开文件

1.下载SumatraPDF 2.设置 3.代码 void MainWindow::on_pushButton_clicked() {QProcess *process new QProcess();QString filePath "C:\\Users\\jude\\Desktop\\su\\11.pdf";QString sumatraPath "C:\\Users\\jude\\Desktop\\su\\SumatraPDF-3.5.2-64.exe&q…

电瓶车火灾频发背后的隐忧

近年来&#xff0c;电瓶车火灾事件频发&#xff0c;不仅严重威胁着人民群众的生命财产安全&#xff0c;也给社会带来了极大的安全隐患。从城市街道到居民小区&#xff0c;电瓶车火灾的阴影无处不在&#xff0c;如何有效防范与自救成为了全社会关注的焦点。 一、电瓶车火灾频发…

linux_L1_linux重启服务器

使用putty登录到linux服务器切换到管理员账号 sudo -s重启命令 reboot

钾盐矿开采与加工过程中的机电设备选型及管理指南

创作不易&#xff0c;您的打赏、关注、点赞、收藏和转发是我坚持下去的动力&#xff01; 在钾盐矿的开采和加工过程中&#xff0c;需要使用多种机电设备以确保生产的顺利进行。这些设备主要用于矿石开采、破碎、运输、选矿以及矿物产品的深加工等过程。以下是钾盐矿常用的一些机…

Leetcode 字母异位词分组

这道题目的意思就是&#xff1a;把包含字母字符相同的单词分到同一组。 算法思路&#xff1a; 使用哈希表来解决。 首先将每个字符串进行排序&#xff0c;将排序之后的字符串作为 key&#xff0c;然后将用 key 所对应的异位词组 作为value。然后我们使用 std::pair 来遍历 键…

AI prompt(提示词)

# 好用的用于学习的AI提示词 ## 费曼学习法 请使用费曼学习法&#xff0c;用简单的语言解释&#xff08;量子力学&#xff09;是什么&#xff0c;并提供一个简单的例子来说明它如何应用 ## 帕累托法则&#xff08;80/20原则&#xff09; 将&#xff08;量子力学&#xff09;最…

基于SSM的大学生心理健康服务平台的设计与实现---附源码75713

目 录 摘要 1 绪论 1.1 研究背景 1.2 研究意义 1.3论文结构与章节安排 2 相关技术介绍 2.1 SSM框架 2.2 Java语言 2.3 MySQL数据库 3系统分析 3.1 可行性分析 3.2 系统功能分析 3.3 系统用例分析 3.4系统流程分析 3.4.1 用户登录流程 3.4.2 数据删除流程 4 系…

P3565 [POI2014] HOT-Hotels

~~~~~ P3565 [POI2014] HOT-Hotels ~~~~~ 总题单链接 ~~~~~ 2024.9.10&#xff1a;DP方程有问题&#xff0c;已修改&#xff0c;同时更新了长链剖分优化版本。 思路 ~~~~~ 设 g [ u ] [ i ] g[u][i] g[u][i] 表示在 u u u 的子树内&#xff0c;距离 u u u 为 i i i 的点的…