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.rhkb.cn/news/422312.html

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

相关文章

整型数组按个位值排序

题目描述 给定一个非空数组(列表)&#xff0c;其元素数据类型为整型&#xff0c;请按照数组元素十进制最低位从小到大进行排序&#xff0c;十进制最低位相同的元司 相对位置保持不变。 当数组元素为负值时&#xff0c;十进制最低位等同于去除符号位后对应十进制值最低位。 输…

Facebook的虚拟现实计划:未来社交的全新视角

随着科技的不断进步&#xff0c;虚拟现实&#xff08;VR&#xff09;正逐步成为我们日常生活的一部分。作为全球领先的社交平台&#xff0c;Facebook正在大力投入虚拟现实技术&#xff0c;以重新定义社交互动的方式。本文将深入探讨Facebook的虚拟现实计划&#xff0c;分析其如…

Mycat2原理介绍

Mycat介绍 Mycat原理 Mycat 核心配置 Scheam.xml 逻辑数据库和节点对应关系配置Server.xml mycat的连接配置Rule.xml. 分片规则 自动分片auto-sharding-long&#xff0c;比如0-10000节点1 &#xff0c;10001-20000节点2枚举分片sahrding-bt-intfile ,比如beijing节点1…

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

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

【数据库】MySQL-基础篇-SQL

专栏文章索引&#xff1a;数据库 有问题可私聊&#xff1a;QQ&#xff1a;3375119339 目录 一、SQL通用语法 二、SQL分类 三、DDL 1.数据库操作 1.1 查询所有数据库 1.2 查询当前数据库 1.3 创建数据库 1&#xff09;案例&#xff1a; 1.4 删除数据库 1.5 切换数据库…

discuz论坛3.4 截图粘贴图片发帖后显示不正常问题

处理方法 source\function 路径下修改function_discuzcode.php function bbcodeurl($url, $tags) 函数 if(!in_array(strtolower(substr($url, 0, 6)), array(http:/, https:, ftp://, rtsp:/, mms://,data:i) 这一句里增加 data:i 即可 function bbcodeurl($url,…

JAVA基础:抽象类,接口,instanceof,类关系,克隆

1 JDK中的包 JDK JRE 开发工具集&#xff08;javac.exe&#xff09; JRE JVM java类库 JVM java 虚拟机 jdk中自带了许多的包&#xff08;类&#xff09; &#xff0c; 常用的有 java.lang 该包中的类&#xff0c;不需要引用&#xff0c;可以直接使用。 例如&#xff1…

Redis面试题整理

Redis 1、Redis主从集群 1.1、搭建主从集群 单节点Redis的并发能力是有上限的&#xff0c;要进一步提高Redis的并发能力&#xff0c;就需要搭建主从集群&#xff0c;实现读写分离 1.2、主从同步原理 当主从第一次同步连接或断开重连时&#xff0c;从节点都会发送psync请求&…

即插即用篇 | YOLOv8 引入组装式Transformer模块AssembleFormer | arXiv 2024

本改进已同步到YOLO-Magic框架! 摘要—早期检测和准确诊断可以预测恶性疾病转化的风险,从而增加有效治疗的可能性。轻微的症状和小范围的感染区域是一种不祥的警告,是疾病早期诊断的重中之重。深度学习算法,如卷积神经网络(CNNs),已被用于分割自然或医学对象,显示出有希…

mp3转文字要怎么处理?使用这4个工具就对了

MP3是音频当中比较常用的格式&#xff0c;如果像将其转换成文字内容&#xff0c;一般的语音转文字工具都是可以完成的。但是音频转换成文字的过程中&#xff0c;它的准确率是会受到像口音&#xff0c;语言&#xff0c;环境音等因素的影响的。所以大家如果想将自己的mp3语音转成…

INIC6081量产工具下载,initio6081开卡软件分享

国内固态硬盘常用&#xff0c;且有量产工具流传出来的主控厂商包括慧荣、群联、点序、英韧、得一微、瑞昱、联芸、迈威、国科、华澜微等等。 每个主控需要用各自对应的量产工具&#xff0c;不同的量产工具支持的闪存颗粒也有差异&#xff0c;因此要根据固态硬盘实际的主控型号…

ESXI8.0 vsphere vcenter 多网卡多网段配置

一般来说服务器至少两块网卡&#xff0c;安装esxi后一种方案是利用闲置网卡建立多上传链路&#xff0c;聚合&#xff0c;另一种是配置多网段进行虚拟机隔离&#xff0c;网上也没找到讲的很清楚的&#xff0c;经过多种尝试终于学会&#xff0c;记录分享一下 首先物理交换机的随…

【idea-安装】

JetBrains官⽹ : https://www.jetbrains.com/ 1.下载idea安装包&#xff0c;下载旧一些的版本&#xff0c;避免新版本的不稳定。 下载下来的安装包是exe格式的&#xff0c;直接点击运行。 点击Next 2.选择要下载的位置&#xff0c;点击下一步。 3.选择⽣成快捷⽅式和建⽴⽂件…

Nginx怎么重新编译添加模块

转自 https://www.php.cn/faq/547300.html

linux_终端输入_几个提高效率的超有用配置

ubuntu为例&#xff1a; 1. 按上下键&#xff0c;补全历史指令 只输入一条历史命令的前几个字母&#xff0c;再按PageUp和PageDown键&#xff0c;就可以在以此字母为前缀的历史命令中上下切换。这个功能非常实用&#xff0c;而且比CTRLR使用起来更友善、更方便。遗憾的是&…

fastadmin 文件上传腾讯云

1-安装腾讯云SDK composer require qcloud/cos-sdk-v5 2-腾讯云配置 <?phpnamespace app\common\controller;use Qcloud\Cos\Client; use think\Controller; use think\Db;class Tencent extends Controller {/*** 上传文件* param $config* param $key* return array*/p…

《卷积神经网络 CNN 原理探秘》

CNN基本原理详解 卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;简称CNN&#xff09;&#xff0c;是一种前馈神经网络&#xff0c;人工神经元可以响应周围单元&#xff0c;可以进行大型图像处理。卷积神经网络包括卷积层和池化层。 卷积神经网络是受…

Visual Studio 设置文件默认编码格式、行尾符等

文章目录 1.命令方式2.EditorConfig配置 1.命令方式 2.EditorConfig配置 微软官方文档 使用EditorConfig方式配置&#xff0c;无需Visual Studio软件自带对EditorConfig的支持&#xff0c;无需插件 将下面.editorconfig文件放在项目根目录下 root true # 所在目录是根目录…

外包干了三年,快要废了。。。

先简单说一下自己的情况&#xff0c;普通本科&#xff0c;在外包干了3年多的功能测试&#xff0c;这几年因为大环境不好&#xff0c;我整个人心惊胆战的&#xff0c;怕自己卷铺盖走人了&#xff0c;我感觉自己不能够在这样蹉跎下去了&#xff0c;长时间呆在一个舒适的环境真的会…

Ubuntu上安装libdc1394-22-dev出现无法定位安装包的解决办法

一、libdc1394-22-dev介绍 libdc1394-22-dev 是一个开发库&#xff0c;用于与IEEE 1394 (FireWire)摄像头进行交互。具体来说&#xff0c;它是 libdc1394 的开发版本&#xff0c;提供了开发者头文件和链接库&#xff0c;方便在应用程序中集成对基于 IEEE 1394 标准的数码相机的…