Java 集合深入理解 (十一) :HashMap之实现原理及hash碰撞

article/2025/11/11 2:38:33

文章目录

    • 前言
    • 哈希表原理
    • 实现示例
    • HashMap实现原理
    • 全篇注释分析
    • 实现注意事项
    • 默认属性分析
    • 属性分析
    • 构造方法分析
    • 重要的put方法
    • 总结

前言

哈希表(hashMap)又叫散列表

  • 是一种非常重要的数据结构基于map接口实现
  • 应用场景及其丰富,本地临时缓存,许多缓存技术(比如memcached)
  • 核心其实就是在内存中维护一张大的哈希表
  • 本文会对java集合框架中HashMap的实现原理进行讲解,从而达到深入理解哈希表

哈希表原理

在讨论哈希表原理之前,先看一下常用数据结构在新增,查找等基础操作对比执行性能
在这里插入图片描述
上图从数据结构上分析时间复杂度,明显看出哈希表的时间复杂度是很低的(不考虑冲突的情况下)

什么是哈希表

散列表(也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。

  • 它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数
  • 存放记录的数组叫做散列表。
  • 给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
    在这里插入图片描述

哈希冲突

  • 对应不同的关键字可能获得相同的hash地址,即 key1≠key2,但是f(key1)=f(key2)。这种现象就是哈希冲突
  • 这种冲突只能尽可能的减少,不能完全避免。因为哈希函数是从关键字集合和地址集合的映像,通常关键字集合比较大,而地址集合的元素仅为哈希表中的地址值。

在这里插入图片描述

实现示例

	public static void main(String[] args) {System.out.println("--------------hashmap--------------");HashMap maps=new HashMap();maps.put("1", "1");maps.put("6", "6");maps.put("3", "3");maps.put("7", "7");maps.put("2", "2");maps.keySet().stream().forEach(m->{System.out.println(maps.get(m));});}
--------------hashmap--------------
1
2
3
6
7

从上面对于这个例子来看

  • hashmap迭代器获取数据,并不是顺序的,而是根据hashcode的计算的地址 然后从首个节点进行往后遍历的

HashMap实现原理

以jdk1.8为例
hashmap的主干是Node数组,node是每个 数组的基本单元,里面包括hash值, key value 并包含 下个node节点(所以说 hashmap是一个单链表数组)

/**
*表,在第一次使用时初始化,并将其大小调整为必要的。分配时,长度总是2的幂。
*(在某些操作中,我们也允许长度为零,以允许当前不需要的引导机制。)
*/
transient Node<K,V>[] table;

Node 节点单元

 static class Node<K,V> implements Map.Entry<K,V> {final int 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;}

在这里插入图片描述

  • 由上可以看出当不存在hash冲突,或者链表长度不大于1,插入 删除 查找时间复杂度为O(1)
  • 链表长度大于1,对于添加操作,其时间复杂度为O(n)
  • jdk1.8之后 为降低时间复杂度,链表长度大于8并且数组长度大于64时,自动降级成为为红黑树,时间复杂度降低为O(lgn)

全篇注释分析

	/***基于哈希表的<tt>映射</tt>接口实现。这个实现提供了所有可选的map操作和许可证*<tt>null</tt>值和<tt>null</tt>键(哈希图类大致相当于<tt>哈希表</tt>,只是*这个类不保证地图的顺序;特别是,它不能保证将随时间保持不变。**<p>此实现为基本操作(<tt>get</tt>和<tt>put</tt>),假定哈希函数在桶中适当分散元素。迭代结束*集合视图需要与视图的“容量”成比例的时间<tt>HashMap</tt>实例(bucket的数量)加上它的大小(bucket的数量键值映射)。因此,设置初始值是非常重要的*如果迭代性能不好,容量太高(或负载系数太低)都会导致时间复杂度降低。**<p>HashMap的实例有两个参数影响其性能:<i>初始容量</i>和<i>负载系数</i>。这个<i>容量</i>是哈希表中的存储桶数,以及*容量只是创建哈希表时的容量。这个<i>加载因子</i>是允许哈希表的填充程度的度量*在其容量自动增加之前获取。当哈希表中的条目超过了加载因子和当前容量时,哈希表被重新格式化(即内部数据)结构)以便哈希表具有大约两倍的*铲斗数量。**<p>一般来说,默认负载系数(.75)提供了良好的性能时间和空间成本之间的权衡。较高的值会降低*空间开销,但会增加查找成本(反映在大多数类的操作,包括<tt>获取</tt>和<tt>放置</tt>)。中的预期条目数*在安装时应考虑地图及其荷载系数设置其初始容量,以尽量减少再冲洗操作。如果初始容量大于最大条目数除以负荷系数,无需再灰分*行动永远不会发生。**<p>如果要在HashMap中存储许多映射例如,使用足够大的容量创建它将允许存储映射要比让它执行更有效*根据需要自动重新灰化以增大桌子。注意,使用许多具有相同{@code hashCode()}的键肯定会减慢速度*降低任何哈希表的性能。当钥匙如果{@link Comparable},这个类可以使用*帮助打破关系的钥匙。**<p><strong>请注意,此实现不同步。</strong>*如果多个线程同时访问哈希映射,则线程从结构上修改映射,它必须外部同步(结构修改是指任何操作*添加或删除一个或多个映射的;只是改变了价值与实例已包含的键关联的不是*结构修改)这通常由在自然封装地图的某个对象上进行同步。**如果不存在这样的对象,则应该使用*{@link Collections#synchronizedMap Collections.synchronizedMap}*方法。这最好在创建时完成,以防止意外对地图的非同步访问:<pre>*Map m=Collections.synchronizedMap(新HashMap(…))</预处理>**<p>这个类的所有“集合视图方法”返回的迭代器是否快速故障:如果在故障发生后的任何时间对地图进行结构修改*迭代器是以任何方式创建的,除了通过迭代器自己的*<tt>remove</tt>方法,迭代器将抛出{@link ConcurrentModificationException}。因此,面对*修改后,迭代器会快速而干净地失败,而不是冒着*在一个不确定的时间里的任意的,不确定的行为*未来。*<p>请注意,不能保证迭代器的快速失败行为一般说来,不可能在未来作出任何硬性保证*存在未同步的并发修改。失败快速迭代器尽最大努力抛出ConcurrentModificationException。*因此,编写依赖于此的程序是错误的其正确性例外:<i>迭代器的快速失败行为应仅用于检测错误。</i>* <p>This class is a member of the* <a href="{@docRoot}/../technotes/guides/collections/index.html">* Java Collections Framework</a>.** @param <K> the type of keys maintained by this map* @param <V> the type of mapped values** @author  Doug Lea* @author  Josh Bloch* @author  Arthur van Hoff* @author  Neal Gafter* @see     Object#hashCode()* @see     Collection* @see     Map* @see     TreeMap* @see     Hashtable* @since   1.2*/

注释分析

  • hashmap是基于map的实现,不保证数据的顺序,特别是,它不能保证将随时间保持不变
  • HashMap的实例有两个参数影响其性能,初始容量和负载系数 ,当哈希表中的条目超过了加载因子和当前容量时,哈希表被重新格式化(即内部数据)结构)以便哈希表具有大约两倍bucket的 数量(数组)
  • 许多具有相同{@code hashCode()}的键肯定会减慢速度降低任何哈希表的性能,因此选择使用
  • hashmap不同步,需要使用Collections.synchronizedMap来保证数据安全
  • 使用迭代器的快速失败机制抛出异常保证数据操作安全

实现注意事项

在分析该篇文章前我们看一下实现的注意事项

/*
*实现注意事项。
*
*这个映射通常充当一个装箱的哈希表,但是当箱子变得太大时,它们会被转化为垃圾箱
*树节点,每个树节点的结构与java.util.TreeMap。大多数方法尝试使用普通的垃圾箱,但是适用时,中继到树节点方法(仅通过检查
*节点的实例)。树状物箱可以穿过与其他方法一样使用,但还支持更快的查找当人口过剩时。然而,由于绝大多数垃圾箱
*正常使用不会过多,检查是否存在在表方法的过程中,树容器可能会被延迟。
*树仓(即其元素均为树节点的仓)是主要按hashCode排序,但在tie的情况下,如果是两个元素属于相同的“C类<C>”,
*然后使用compareTo方法进行排序(我们通过反射保守地检查泛型类型以验证这个——请参见方法CompariableClassfor)。增加的复杂性
*在提供最坏情况O(logn)时,树仓的数量是值得的当键具有不同的哈希或
*因此,在这种情况下,性能会优雅地下降hashCode()方法的意外或恶意使用
*返回分布不均匀的值,以及哪些键共享一个哈希码,只要它们也是
*可比(如果这两个都不适用,我们可能会浪费一个月的时间在时间和空间上两个因素与不采取行动相比
*注意事项。但已知的唯一案例来自于糟糕的用户编程实践已经非常缓慢,这使得
*差别不大。)
*因为树节点的大小是常规节点的两倍,所以我们仅当容器包含足够的节点以保证使用时才使用它们
*(参见TREEIFYèu阈值)。当它们变得太小(由于移除或调整大小)它们被转换回普通垃圾箱。在
*使用分布良好的用户hashcode,树容器很少使用。理想情况下,在随机哈希码下
*箱中的节点遵循泊松分布
* (http://en.wikipedia.org/wiki/Poisson_distribution)带着
*默认调整大小的参数平均约为0.5  阈值为0.75,但由于调整粒度。忽略方差,预期列表大小k的出现次数为(exp(-0.5)*pow(0.5,k)/阶乘(k))。第一个值是:
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
*更多:不到千万分之一
*树bin的根通常是它的第一个节点。然而,有时(当前仅在Iterator.remove上),根可能位于其他位置,但可以通过父链接恢复(方法TreeNode.root())。
*所有适用的内部方法都接受哈希代码作为参数(通常由公共方法提供),允许
*它们可以在不重新计算用户哈希码的情况下互相调用。大多数内部方法也接受“tab”参数,即通常是当前表,但可能是新表或旧表
*调整大小或转换。当垃圾箱列表被树化、拆分或未经审核时,我们会保留它们具有相同的相对访问/遍历顺序(即字段
*Node.next)以更好地保留局部性,并简化处理调用
*迭代器.remove。在插入时使用比较器,以保持总订购量(或尽可能接近此处要求)再平衡,我们比较类和身份密码系紧断路器。
*简单模式和树模式之间的使用和转换是由于子类LinkedHashMap的存在而变得复杂。看到了吗对于定义为在插入时调用的钩子方法,
*允许LinkedHashMap内部否则的话,它们将与这些力学无关(这也是要求将映射实例传递给某些实用程序方法
*可能会创建新节点。)
*像基于SSA的编码风格这样的并发编程很有帮助避免在所有扭曲指针操作中出现别名错误。
*/

第一段说明为什么要使用红黑树
红黑树的插入、删除和遍历的最坏时间复杂度都是log(n),因此,在意外或者恶意使用导致hashCode()方法返回值的分布很糟糕,以及在那些许多key共享一个hashCode的情况下,只要Key具有可比性,性能的下降将会是"优雅"的。(如果这两种方法都不适用,与不采取预防措施相比,我们可能会浪费大约两倍的时间和空间。但目前所知的唯一案例来自于糟糕的用户编程实践,这些实践已经非常缓慢,以至于没有什么区别。)

第二段说明链表转化为红黑树的阈值设置为8
当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以选择8。

默认属性分析

初始化容量(数组的容量大小)

/*** 默认初始容量-必须是2的幂*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; aka 16

为什么默认初始容量-必须是2的幂,

  • 做位与运算(&)时,设置容量为2的幂指数,避免了hash碰撞产生链表,使得结果可以均匀分布 ,提高了查询效率(i = (n - 1) & hash) 确定下标 index
  • 是16如果是8也可以均匀分布,是小于10的,研究了所有集合包的类,基本都大于10的,这个也是考虑我们在使用的大多数,而且其实都给我们提供设置了初始化容量的构造方法

初始化链表数组长度

  else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}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"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

这里如果是默认值的话添加值,创建一个和容量大小的数组长度也就是16.也就是整个hashmap第一次扩容,在最理想的情况下 链表存11个数据,然后每个数组格存一个也就是15个 总的就是26个;因此扩容条件一是要到扩容的阈值,然后并且添加数据的数组格不为空
最大容量(数组的容量大小)

	/***如果隐式指定了更高的值,则使用最大容量由带参数的构造函数之一。*必须是2的幂<=1<<30。*/static final int MAXIMUM_CAPACITY = 1 << 30;

为什么最大容量1 << 30,我想它有两个点值得思考

  • 减少hash碰撞 ,HashMap在确定链表数组下标Index的时候,采用(i = (n - 1) & hash),所以length为2的指数幂的时候才能较均匀的分布元素,所以HashMap规定了其容量必须是2的n次方
  • HashMap规定了其容量是2的n次方,所以作者采用的是位运算<<来控制HashMap的大小,并且因此提高了Java的处理速度;HashMap内部是由数组构成,Java的数组下标是由Int表示的;所以容量肯定是不大于int最大值,进而采用1 << 30 1 073 741 824 值

扩容加载因子

  /*** 构造函数中未指定时使用的负载因子。*/static final float DEFAULT_LOAD_FACTOR = 0.75f;

加载因子是表示Hsah表中元素的填满的程度

  • 加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.
  • 反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.
  • 加载因子和初始化容量相乘使用得到扩容阈值;根据业务使用,太小容易造成扩容,重新存储链表空间很耗时间的

链表转换树的阈值

  /*** 这个值是为存储箱(链表)使用树不是列表的存储箱计数阈值,当将元素添加到至少有这么多节点的容器中时,容器将转换为树* 并且值必须大于2,且至少应为8,以符合中的假设关于转换回普通链表的树木移除收缩。*/static final int TREEIFY_THRESHOLD = 8;

这个阈值至少大于8的原因

  • 和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。
  • 从注释解释在负载因子0.75(HashMap默认)的情况下,单个hash槽内元素个数为7的概率0.00000094 ,当元素个数为8时,为0.00000006;更小,概率不到千万分之一;证明大于8概率是非常小的,还是存在 ;hash碰撞次数多,并且遍历效率也会降低

为什么不一开始就直接使用红黑树而不用阈值

  • 红黑树中的TreeNode是链表中的Node所占空间的2倍,红黑树的查找效率为o(logN),要优于链表的o(N);
  • 当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高。

要寻找一种时间和空间的平衡,即在链表长度达到一个阈值之后再转换为红黑树。

树转换链表的阈值

	/***存储过程中取消检测(拆分)存储箱的存储箱计数阈值*调整大小操作。应小于阈值,并且多数6目筛下用收缩检测去除。*/static final int UNTREEIFY_THRESHOLD = 6;

为什么不直接用上面 TREEIFY_THRESHOLD 进行树的收缩
想的主要原因还是防止频繁的转换,从而导致速率降低

最小树容量

  /***最小的树容量,箱子可以被树化。(否则,如果bin中的节点太多,则会调整表的大小。)*应至少为4*TREEIFY_THRESHOLD阈值以避免冲突在调整大小和树化阈值之间。 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD*/static final int MIN_TREEIFY_CAPACITY = 64;

桶中的Node被树化时最小的hash表容量。

  • 也就是链表变红黑树的条件除了链表的长度需要大于8,并且 数组容量大于64。
  • 这个值不能小于 4 * TREEIFY_THRESHOLD(8)

属性分析

下面属性就不怎么分析,在实现hashmap时,具体用这些属性进行存储记录,都是比较基础,需要在应用时才能看到使用地方

  /*** 以node节点的数组,用于扩容 数组等*/transient Node<K,V>[] table;/*** 保留缓存的 entrySet(). 请注意,使用了AbstractMap字段* 的 keySet() and values() 方法*/transient Set<Map.Entry<K,V>> entrySet;transient int size;transient int modCount;/*** 要调整大小的下一个大小值(容量*负载系数)。** @serial*/// (序列化后javadoc描述为true。// 此外,如果尚未分配表数组,则// 字段保存初始数组容量,或零表示// 默认容量(初始容量)int threshold;

构造方法分析

无参构造方法

  • 直接new hashmap时,和其他集合有一点不太一样的是,并没有调用其他有参构造方法对容量值 等默认值进行赋值。
  • 而这里只对加载因子做了赋值,这种方式是适配元素创建散列表
 /*** 用默认的初始容量构造一个空的<tt>HashMap</tt>* (16) 和默认加载因子为 (0.75).*/public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // 所有其他字段均为默认值}

有参构造方法

  • 指定一个空的hashmap,并指定出 容量及 默认负载系数
  • 传入的容量(initialCapacity)一定要小于默认容量(MAXIMUM_CAPACITY) 否则也是按默认容量进行赋值,在构造参数时,threshold这个值相当于默认容量 ,只有在 数据改变时,这个参数扩容的临界值,=容量*填充因子
  • 对于hashmap 直接传入Map的,putMapEntries 方法解析,我放到解析 putAll 方法时解析
    /*** 用指定的初始值构造一个空的<tt>HashMap</tt>* 容量和默认负载系数(0.75)。** @param  initialCapacity the initial capacity.* @throws IllegalArgumentException if the initial capacity is negative.*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}

tableSizeFor方法
该方法用于进行找到大于等于initialCapacity的最小的2的幂,结合上面的容量解释就想得明白,提高查询效率 减少hash碰撞

  /*** 返回给定目标容量的二次幂。*/static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

需要理解上面的代码,我们先了解一下位运算(>>>)这个是什么

>>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0
比如5的二进制是1015>>>2表示右移2位,变成001,即为1

按二进制形式把所有的数字向右移动对应位数,低位移出(舍弃),高位的空位补零。
从另一个角度来分析,它向右移动2 位,其实就是除以2 的2 次方

然后在理解该代码的意思

  • 对n右移1位:001xx…xxx,再位或:011xx…xxx
  • 对n右移2为:00011…xxx,再位或:01111…xxx
  • 此时前面已经有四个1了,再右移4位且位或可得8个1
  • 同理,有8个1,右移8位肯定会让后八位也为1。

综上可得,该算法让最高位的1后面的位全变为1。最后再让结果n+1,即得到了2的整数次幂的值了。由于int是32位,所以>>>16便能满足。
在这里插入图片描述
下面有两个需要注意的点

  • 不断右移位进行或运算,一直右移到16位进行运算的原因都是为了取到最近的 2的整数次幂的值,在这里 容量为12,在右移两位时,已经计算出最近的 2的整数次幂 后面的不需要了,在继续移动16位进行对比的原因,也是判断容量的大小
  • 为什么要int n = cap - 1;让cap-1再赋值给n的目的是另找到的目标值大于或等于原值,我在推算时,就想过直接用8去算,如果不减1,则该算法运算出来则容量为16

其次这种方法的效率非常高,也是得益于位运算操作

重要的put方法

从put方法中可以看出 分为两个步骤 一是根据key计算出hash值 、二 增加value值

  /*** 将指定值与此映射中的指定键相关联。* 如果映射以前包含键的映射,则旧的值被替换。* * @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with <tt>key</tt>, or*         <tt>null</tt> if there was no mapping for <tt>key</tt>.*         (A <tt>null</tt> return can also indicate that the map*         previously associated <tt>null</tt> with <tt>key</tt>.)*/public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}

计算hashcode值

   /*** 计算key.hashCode()并将哈希的高位(XOR)展开降低。因为这个表使用了两个掩蔽的幂,所以* 仅在当前掩码上方的位中变化的哈希将总是碰撞(在已知的例子中有一组浮点键把连续的整数放在小表格里。)所以我们* 应用扩展高位影响的变换向下。在速度、效用和效率之间有一个折衷钻头扩展质量。因为许多常见的散列* 已经合理分配(因此不要从中受益因为我们用树来处理大量的在箱子里发生碰撞时,我们只是对箱子里的一些移位位进行异或运算* 减少系统性损失的最便宜的方法,以及合并最高位的影响,否则由于表边界的原因,不能在索引计算中使用*/static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

扰动函数
只做一次16位右位移异或混合,而不是四次。具体的可以看看
JDK 源码中 HashMap 的 hash 方法原理是什么?
putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//声明局部变量  tab(散列表),  p 链表节点,n 记录散列表的长度  i 得到数组指针Node<K,V>[] tab; Node<K,V> p; int n, i;//  将 成员属性中的散列表节点赋值给局部变量 并判断数组节点下是否为空,(也就是判断数组节点下是否是第一次增加数据)if ((tab = table) == null || (n = tab.length) == 0)//resize 方法创建一个二次幂整数的散列表 (默认为16)n = (tab = resize()).length;//判断当前指针下的table数组值是否为空并赋值给局部p链表,(n - 1) & hash等价于hash % n ,jdk1.7就是这么写的 取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)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 && //判断插入数据的 hash 和当前位置hash是否相等//判断插入key值和当前节点的key值相等,那就替换值((k = p.key) == key || (key != null && key.equals(k))))//直接替换了节点e = p;else if (p instanceof TreeNode)//当前节点的 key 和要插入的 key 不一样,判断当前节点是红黑树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) // 判断节点是否超过默认阈值treeifyBin(tab, hash);//超出了之后就将当前链表转换为树,注意转换树的时候,如果当前数组的长度小于MIN_TREEIFY_CAPACITY(默认 64),会触发扩容而不转换树break;}//如果当前遍历到的数据和要插入的数据的 key 是一样,和上面之前的一样,赋值给变量 e,只做替换,并跳出if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { //key已经存在V oldValue = e.value;//将当前节点的值赋值给 oldvalueif (!onlyIfAbsent || oldValue == null)e.value = value; //将当前要插入的 value 替换当前的节点里面值afterNodeAccess(e); //回调以允许LinkedHashMap发布操作   主要是通知 LinkedHashMap将节点移到最后一个return oldValue;}}++modCount;//操作数增加1if (++size > threshold)resize();//如果当前的 hash表的长度已经超过了当前 hash 需要扩容的长度,然后进行重新扩容(首次增加数据不会走这里,在2步骤已经扩容)afterNodeInsertion(evict);//调用LinkedHashMap判断是否把可能把老大除掉return null;}

上面的注释做一个拆分

  1. 声明局部变量 tab(散列表), p 链表节点,n 记录散列表的长度 i 得到数组指针
  2. 将 成员属性中的散列表节点赋值给局部变量 并判断数组节点下是否为空,(也就是判断数组节点下是否是第一次增加数据)
  3. 判断当前指针下的table数组值是否为空并赋值给局部p链表,
    (n - 1) & hash等价于hash % n ,jdk1.7就是这么写的 取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)
  4. 代表要插入的数据所在的位置是有内容的,则就要做判断处理
    1.声明了一个节点, 泛型
    2.判断插入数据的 hash 和当前位置hash是否相等,判断插入key值和当前节点的key值相等,那就替换节点
    3.当前节点的 key 和要插入的 key 不一样,当前节点是红黑树 则插入节点
    4.如果不是树节点,代表当前是一个链表,那么就遍历链表,找到末尾节点
a.判断节点是否超过默认阈值,超出了之后就将当前链表转换为树,注意转换树的时候,如果当前数组的长度小于MIN_TREEIFY_CAPACITY(默认 64),会触发扩容而不转换树
b. 如果当前遍历到的数据和要插入的数据的 key 是一样,和上面之前的一样,赋值给变量 e,只做替换,并跳出循环
  1. 操作数增加1
  2. 如果当前的 hash表的长度已经超过了当前 hash 需要扩容的长度,然后进行重新扩容(首次增加数据不会走这里,在2步骤已经扩容)
  3. 调用LinkedHashMap判断是否把可能把node除掉

总结

这篇分析jdk1.8的hashmap的源码,比jdk1.7的源码看来痛苦多,我的直观感受在于,多了很多位运算,不能直接看懂;还有在 判断语句中赋值 例如 if ((tab = table) == null || (n = tab.length) == 0) ;但我觉得还是有很多值得我们深研的地方,包括hashmap虽然允许我们存储空元素 ,但还是别把数据设置为空好点,希望各位码农一起研究一起提高开发水平。

还有其余的 扩容,红黑树转换 get方法,我放到另外篇文章深究
Java 集合深入理解 (十二) :HashMap之扩容 功能


http://chatgpt.dhexx.cn/article/VjbvVZcX.shtml

相关文章

java基础篇 - HashMap 理解Hash碰撞

HashMap是大家都在用&#xff0c;面试的时候也经常会被考的考点&#xff0c;在这篇文章中说下HashMap的hash碰撞和减轻碰撞的优化。 1、什么是hash碰撞 在解释Hash碰撞之前先说一下hashmap的存储结构、添加和检索是怎么实现的 1.1HashMap的存储结构 HashMap的存储结构是En…

大白话解释hash碰撞是什么以及如何解决

一、Hash如何存数据 hash表的本质其实就是数组&#xff0c;hash表中通常存放的是键值对Entry。 这里的id是个key&#xff0c;哈希表就是根据key值来通过哈希函数计算得到一个值&#xff0c;这个值就是下标值&#xff0c;用来确定这个Entry要存放在哈希表中哪个位置。 二、Ha…

hash碰撞的概率推导(生日攻击生日问题)

1.关于hash碰撞 哈希碰撞是指&#xff0c;两个不同的输入得到了相同的输出&#xff1b; hash碰撞不可避免&#xff0c;hash算法是把一个无限输入的集合映射到一个有限的集合里&#xff0c;必然会发生碰撞&#xff1b; 2.碰撞概率的问题描述的其他形式 n个球&#xff0c;&…

Hash碰撞(冲突)的解决方案

hash算法就是&#xff0c;用同一个哈希函数计算&#xff1a; 两个相同的值&#xff0c;计算出的hash值一定相同&#xff0c; 两个不同的值&#xff0c;计算出的hash值可能不同&#xff0c;也可能相同&#xff0c;当相同时就是hash冲突 一、链式寻址法 也叫“拉链法”&#…

MD5 hash碰撞实现解密

目录 1.前言 2.MD5 hash单个碰撞解密 3.MD5 hash批量碰撞解密 1.前言 在日常渗透中,获取到后台密码往往是加密的,常见的就是MD5加密,常见的做法我们会使用在线网站去解密,常用的有cmd5,somd5,cmd5对于一些密文是要收费的,有时我们就想白嫖。 这时我们会用so…

哈希碰撞+mysql_HashMap之Hash碰撞冲突解决方案及未来改进

HashMap位置决定与存储 通过前面的源码分析可知&#xff0c;HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行put(String,Obect)方法 时&#xff0c;系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法&am…

Hash碰撞概率

计算Hash冲突的概率 虽然已经很多可以选择的Hash函数,但创建一个好的Hash函数仍然是一个活跃的研究领域。一些Hash函数是快的,一些是慢的,一些Hash值均匀地分布在值域上,一些不是。对于我们的目的,让我们假设这个Hash函数是非常好的。它的Hash值均匀地分布在值域上。 在这…

HashMap之Hash碰撞

详细理解了Hash碰撞及处理方法 为什么会出现hash碰撞 在hash算法下,假设两个输入串的值不同,但是得到的hash值相同, 即会产生hash碰撞 一个很简单的例子: 假设你自己设计了一个计算hash的算法toHashValue(String). 是取的输入值的Unicode编码值(当然实际的情况会比这复杂很…

hashmap存储方式 hash碰撞及其解决方式

1.Map 的存储特点 在 Map 这个结构中&#xff0c;数据是以键值对&#xff08;key-value&#xff09;的形式进行存储的&#xff0c;每一个存储进 map 的数据都是一一对应的。 创建一个 Map 结构可以使用 new HashMap() 以及 new TreeMap() 两种方式&#xff0c;两者之间的区别…

Java Hash 碰撞

散列函数&#xff08;英语&#xff1a;Hash function&#xff09;又称散列算法、哈希函数&#xff0c;是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要&#xff0c;使得数据量变小&#xff0c;将数据的格式固定下来。 该函数将数据打乱混合…

通俗解释hash碰撞是什么以及如何解决

Hash如何存数据 hash表的本质其实就是数组&#xff0c;hash表中通常存放的是键值对Entry。 如下图: 这里的学号是个key&#xff0c;哈希表就是根据key值来通过哈希函数计算得到一个值&#xff0c;这个值就是下标值&#xff0c;用来确定这个Entry要存放在哈希表中哪个位置。 H…

Hash碰撞

Hash碰撞 什么是Hash碰撞 Hash碰撞是指两个不同的输入值&#xff0c;经过哈希函数的处理后&#xff0c;得到相同的输出值&#xff0c;这种情况被称之为哈希碰撞。 例如&#xff1a;两个不同的对象&#xff08;object1和object2的值&#xff09;经过Hash函数计算后的&#xf…

浅谈“越权访问”

一&#xff1a;漏洞名称&#xff1a; 越权访问漏洞 描述&#xff1a; 越权访问&#xff0c;这类漏洞是指应用在检查授权&#xff08;Authorization&#xff09;时存在纰漏&#xff0c;使得攻击者在获得低权限用户帐后后&#xff0c;可以利用一些方式绕过权限检查&#xff0c;访…

逻辑越权——垂直、水平越权

水平越权&#xff1a;通过更换的某个ID之类的身份标识&#xff0c;从而使A账号获取&#xff08;修改、删除等&#xff09;B账号数据。 垂直越权&#xff1a;使用低权限身份的账号&#xff0c;发送高权限账号才能有的请求&#xff0c;获得其高权限的操作。 未授权访问&#xff1…

横向越权和纵向越权(水平越权、垂直越权)

越权&#xff1a;顾名思义&#xff0c;就是获得了本不应该有的权限。 我们都喜欢创造一些复杂的词汇&#xff0c;而实际上这些词就是一个代词&#xff0c;根本没有那么复杂。 越权漏洞往往是基于业务逻辑的漏洞&#xff0c;这样的漏洞很难被WAF保护。 越权的分类 按照方向…

越权访问

目录 概念 分类 pikachu--水平越权 源码分析 pikachu---垂直越权 源码分析 概念 越权访问&#xff08;Broken Access Control,BAC&#xff09;是web中一种常见的漏洞&#xff0c;且越权漏洞属于逻辑漏洞&#xff0c;是由于权限校验的逻辑不够严谨导致的&#xff0c;所以越…

Web 攻防之业务安全:越权访问漏洞 测试.

Web 攻防之业务安全&#xff1a;越权访问漏洞 测试. 由于没有对用户权限进行严格的判断&#xff0c;导致低权限的账号&#xff08;比如普通用户&#xff09;可以去完成高权限账号&#xff08;比如超级管理员&#xff09;范围内的操作。&#xff08;比如&#xff1a;通过更换的…

越权漏洞系列

0x01&#xff1a;越权的定义 越权漏洞是我们在测试过程中遇到比较多的漏洞&#xff0c;我们可以这样来理解越权漏洞&#xff0c;一个用户A一般只能够对自己本身的信息进行增删改查&#xff0c;然而由于后台开发人员的疏忽&#xff0c;没有在信息进行增删改查时候进行用户判断&…

java越权问题

关于java项目越权问题 问题描述实现思路具体代码 项目上线前做了安全扫描&#xff0c;安全部门扫描出一个关于越权的问题。这个问题是在刚开始开发接口的时候没有考虑到的一个事情。&#xff08;此项目是有关于用户所拥有的项目和活动权限的问题。&#xff09; 问题描述 首先…

越权 漏洞

一、越权漏洞描述 越权访问&#xff08;Broken Access Control&#xff0c;简称 BAC&#xff09;是 Web 应用程序中一种常见的漏洞&#xff0c;由于其存在范围广、危害大&#xff0c;被 OWASP 列为 Web 应用十大安全隐患的第二名。 该漏洞是指应用在检查授权时存在纰漏&#x…