HashMap原理。图文并茂式解读。这些注意点你一定还不了解

2024-08-29 14:32

本文主要是介绍HashMap原理。图文并茂式解读。这些注意点你一定还不了解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

[TOC]

概述

本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap。HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现。HashMap 允许 null 键和 null 值,在计算哈键的哈希值时,null 键哈希值为 0。HashMap 并不保证键值对的顺序,这意味着在进行某些操作后,键值对的顺序可能会发生变化。另外,需要注意的是,HashMap 是非线程安全类,在多线程环境下可能会存在问题。

属性详解

DEFAULTINITIALCAPACITY 默认初始容量MAXIMUM_CAPACITY 最大容量DEFAULTLOADFACTOR 默认负载因子TREEIFY_THRESHOLD 一个桶的树化阈值(超过此值会变成红黑树)UNTREEIFY_THRESHOLD 一个树的链表还原阈值(小于此值在resize的时候会变回链表)MINTREEIFYCAPACITY 哈希表的最小树形化容量(为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD)

table

HashMap中的数组(hash表)。hash表的长度总是在2^n。至于原因吗,后面专门会说的。数组里存储的是Node节点的数据

entrySet

Node 节点构成的 set

size

当前map中存储节点的数据

modCount

hashMap发生结构性变化的次数,节点转红黑树、扩容等操作。

threshold、loadFactor

扩容阙值和装载因子。

源码知识点必备

getGenericInterfaces和getInterfaces区别

getGenericInterfaces获取直接接口getInterfaces获取所有接口

ParameterizedType

是Type的子接口,表示一个有参数的类型。就是我们俗称的泛型。实现这个接口的类必须提供equals方法。

getRawType

返回最外层<>前面那个类型,即Map 的Map。

getActualTypeArguments

获取“泛型实例”中<>里面的“泛型变量”(也叫类型参数)的值,这个值是一个类型。因为可能有多个“泛型变量”(如:Map ),所以返回的是一个Type[]。注意:无论<>中有几层<>嵌套,这个方法仅仅脱去最外层的<>,之后剩下的内容就作为这个方法的返回值,所以其返回值类型是不确定的。

getOwnerType

获得这个类型的所有者的类型,主要对嵌套定义的内部类而言。列如对java.util.Map.Node 调用getOwnerType方法返回的是interface java.util.Map接口

comparableClassFor

HashMap类中有一个comparableClassFor(Object x)方法,当x的类型为X,且X直接实现了Comparable接口(比较类型必须为X类本身)时,返回x的运行时类型;否则返回null。通过这个方法,我们可以搞清楚一些与类型、泛型相关的概念和方法

(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)

hashCode与自己的高16为进行异或 。 这样更分散ps:& : 全部为1则为1,否则为0 偏0| : 有一个为1则为1,否则为0 偏1^ : 相同为0 不同为1 更加均衡。 均匀(分散)

hash表维护

在文章开头我们就解释了HashMap中table就是我们的hash表。直观上我们可以理解成一个开辟空间的数组。HashMap通过hash(key)这个方法获取hash值。然后通过hash值确定key在hash表中的位置((n - 1) & hash)。综合上图我们也会发现问题了。key的个数是无限的。但是我们的hash表是有限的。如何能保证hash(key)不会落在同一个位置呢。答案是不能。换句话说就是我们hash(key)无法保证。也就是hashMap会发生hash碰撞的。hash函数只能尽量避免hash碰撞。上面的(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)就是为了让hash更加分散点。这一点上面也作出了解释。

HashMap 数组长度是2^n ?

上面解释了hashmap中hash函数为什么要^ 。 那么深度源码的小伙伴可能会问,为什么hashmap默认容量是16以及后期每次扩容的时候为什么是翻倍扩容。简而言之。为什么hashMap数组长度永远是2的倍数呢。上面我们知道如何通过hash确定在数组中位置的。(n - 1) & hash关于这个n是数组的长度,hash就是key值通过hash函数计算出来的hash值。& 运算规则是: 全部为1则为1,否则为0假设目前hashMap容量是16 , 我们来看看在扩容前后我们key的在是数组中的索引。扩容前后经过图片鲜明的对比我们发现,扩容前后是不会影响原来数据(高位为0)的索引位置的。这里要注意的是并不是说所有数据不受影响,只要原来从右至左第五位为0的hash会受影响,其他不会。这样大大减少了数组位置调换的操作。性能上也大大的提高了。从这里也可以看出hashmap容量越大,扩容是越复杂,因为容量越大,需要换位置的索引越多。那么如果我们扩容是不是选择扩大2倍 , 我们看看会发生什么样情况。上图中是有16扩展成了24容量。这个时候我们会发现除了(从右至左)第五位以为第四位的数据也发生了变化。这样造成的接口是第四位和第五位的数据都会变化。这样增加了索引位置的数量。所以我们需要在每次扩容为原来的2倍。

神奇的hashmap遍历

做Java的肯定会遇到的一种情况是,为什么我的map遍历的顺序和我添加的顺序不一致呢。有时候我们做列表展示的时候对顺序是有要求的。但是hashmap偏偏和我们想的不一样。今天华仔带你看看为什么会出现这种神奇的遍历。

public final void forEach(Consumer<? super K> action) {Node<K,V>[] tab;if (action == null)throw new NullPointerException();if (size > 0 && (tab = table) != null) {int mc = modCount;for (int i = 0; i < tab.length;   i) {for (Node<K,V> e = tab[i]; e != null; e = e.next)action.accept(e.key);}if (modCount != mc)throw new ConcurrentModificationException();}
}

从上面的代码我们可以看出来hashmap在遍历时候,是先遍历数组然后取到数组中链表(红黑树)按照顺序获取node节点的。也即是说我们先按数组再按链表顺序。而不是按照你添加先后的顺序。而上面我们了解添加的node决定其位置的是key的hash值。所以这就解释了为什么hashmap遍历的时候和我们添加不一致的了。

put 流程跟踪


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

其他方法原理是相同的。值得注意的是remove后临界情况会发生红黑树转链表。所以转红黑树的这个阙值的选取有时候会影响性能的高低。下面看看put的实际源码吧。拜读下大佬的代码。

上面的代码可以看出来put实际调用的方法是putVal();int hash : key对应的hash值K key, : keyV value, : valueonlyIfAbsent : 如果存在则忽略,默认false表示新值会覆盖旧值boolean evict: 表示是否在构造table时调用


/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);
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);else {for (int binCount = 0; ;   binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}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;
}

寒暄一句

个人几天时间总结的,有网上前辈的总结,也有加入个人的想法。再次申明:以上图片部分来自网络。

加入战队

加入战队

微信公众号

微信公众号

这篇关于HashMap原理。图文并茂式解读。这些注意点你一定还不了解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1118131

相关文章

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

MCU7.keil中build产生的hex文件解读

1.hex文件大致解读 闲来无事,查看了MCU6.用keil新建项目的hex文件 用FlexHex打开 给我的第一印象是:经过软件的解释之后,发现这些数据排列地十分整齐 :02000F0080FE71:03000000020003F8:0C000300787FE4F6D8FD75810702000F3D:00000001FF 把解释后的数据当作十六进制来观察 1.每一行数据

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

hdu4407容斥原理

题意: 有一个元素为 1~n 的数列{An},有2种操作(1000次): 1、求某段区间 [a,b] 中与 p 互质的数的和。 2、将数列中某个位置元素的值改变。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.Inpu

hdu4059容斥原理

求1-n中与n互质的数的4次方之和 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.PrintWrit

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

SpringMVC入参绑定特别注意

1.直接在controller中定义一个变量,但是此种传输方式有一个限制就是参数名和请求中的参数名必须保持一致,否则失效。 @RequestMapping("test2")@ResponseBodypublic DBHackResponse<UserInfoVo> test2(String id , String name){UserInfoVo userInfoVo = new UserInf