最新要闻
- 世界要闻:苹果WWDC来了!iOS 17有三大变化
- 新消息丨葡萄健康栽培与病虫害防控(关于葡萄健康栽培与病虫害防控的简介)
- 每日快看:318川藏线巨石滚落砸烂一轿车:车内人员躲过一劫
- 荣耀90 Pro真机曝光:“星钻银”配色耀眼 灵感来自珠宝王冠-环球快看点
- 马斯克称特斯拉电动皮卡将在今年开始交付 不打算卸任CEO-最新资讯
- 比用毛巾还便宜 不怕有味儿:大牌洗脸巾7.9元100抽狂促
- 美国教授用ChatGPT判定学生论文抄袭:结果尴尬!聪明反被聪明误|今日热门
- 天天快资讯:时隔3年再“发车”,“北斗专列”如何升级?
- 概念动态|机器人新增“比亚迪概念”
- 世界球精选!你会买吗?澳航推“邻座无人”服务 最低仅140元
- 十年果粉换OPPO Find X6 Pro后直呼惊艳:果断把iPhone 14 Pro挂闲鱼卖掉|世界聚看点
- 视讯!拳头性别歧视案尘埃落定 将赔偿每位女性最多15万刀
- 男子为稳坐榜一大哥骗取乙方百万:全部打赏给女主播 被判刑
- 世界报道:巴西貘被饲养员挠痒一脸舒适 网友:长得东拼西凑但依然很萌
- 上海市首届中青年工程师创新创业大赛启动
- 小米发布米家空调巨省电2匹:新一级能效 一年省380度电
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
京东太猛,手写hashmap又一次重现江湖-全球今日报
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版为您奉上珍贵的学习资源 :
(资料图片)
免费赠送 :《尼恩Java面试宝典》持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备免费赠送 :《尼恩技术圣经+高并发系列PDF》,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
京东太猛,手写hashmap又一次重现江湖
说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如京东、极兔、有赞、希音、百度、网易的面试资格,遇到一个很重要的面试题:
手写一个hashmap?
尼恩读者反馈说,之前总是听人说,大厂喜欢手写hashmap、手写线程池,这次终于碰到了。
和线程池的知识一样,hashmap既是面试的核心知识,又是开发的核心知识。
手写线程池,之前已经通过博客、公众号的形式已经发布:
网易一面:如何设计线程池?请手写一个简单线程池?
在这里,老架构尼恩再接再厉,和架构师唐欢一块,给大家做一下手写hashmap系统化、体系化的线程池梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典》V68版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请关注本公众号 【技术自由圈】获取,暗号:领电子书
手写极简版本的HashMap
如果对HashMap理解不深,可以手写一个极简版本的HashMap,不至于颗粒无收
尼恩给大家展示,两个极简版本的首先HashMap
- 一个GoLang手写HashMap极简版本
- 一个Java手写HashMap极简版本
一个GoLang手写HashMap极简版本
设计不能少,首先,尼恩给大家做点简单的设计:
如果确实不知道怎么写, 可以使用 Wrapper 装饰器模式,把Java或者Golang内置的 HashMap包装一下,然后可以交差了。
如果是使用Go语言实现的话,具体实现方式是通过Go语言内置的map来实现,其中key和value都是int类型。
以下是一个Go语言版本 简单的手写HashMap示例:
package mainimport "fmt"type HashMap struct { data map[int]int}func NewHashMap() *HashMap { return &HashMap{ data: make(map[int]int), }}func (h *HashMap) Put(key, value int) { h.data[key] = value}func (h *HashMap) Get(key int) int { if val, ok := h.data[key]; ok { return val } else { return -1 }}func main() { m := NewHashMap() m.Put(1, 10) m.Put(2, 20) fmt.Println(m.Get(1)) // Output: 10 fmt.Println(m.Get(3)) // Output: -1}
这个HashMap实现了Put方法将key-value对存储在map中,Get方法从map中获取指定key的value值。
为啥要先说go语言版本, Go性能高、上手快,未来几年的Java开发,理论上应该是 Java、Go 并存模式, 所以,首先来一个go语言的版本。
当然,以上的版本,太low了。
这样偷工减料,一定会被嫌弃。 只是在面试的时候,可以和面试官提一嘴, 咱们对设计模式还是很娴熟滴。
既然是手写 手写HashMap ,那么就是要从0开始,自造轮子。接下来,来一个简单版本的Java手写HashMap示例。
一个Java手写HashMap极简版本
设计不能少,首先,尼恩给大家做点简单的设计:
- 数据模型设计:
设计一个Entry数组来存储每个key-value对,其中每个Entry又是一个链表结构,用于解决hash冲突问题。
- 访问方法设计:
设计Put方法将key-value对存储在map中,Get方法从map中获取指定key的value值。
以下是一个简单的Java手写HashMap示例:
public class MyHashMap { private Entry[] buckets; private static final int INITIAL_CAPACITY = 16; public MyHashMap() { this(INITIAL_CAPACITY); } @SuppressWarnings("unchecked") public MyHashMap(int capacity) { buckets = new Entry[capacity]; } public void put(K key, V value) { Entry entry = new Entry<>(key, value); int bucketIndex = getBucketIndex(key); Entry existingEntry = buckets[bucketIndex]; if (existingEntry == null) { buckets[bucketIndex] = entry; } else { while (existingEntry.next != null) { if (existingEntry.key.equals(key)) { existingEntry.value = value; return; } existingEntry = existingEntry.next; } if (existingEntry.key.equals(key)) { existingEntry.value = value; } else { existingEntry.next = entry; } } } public V get(K key) { int bucketIndex = getBucketIndex(key); Entry existingEntry = buckets[bucketIndex]; while (existingEntry != null) { if (existingEntry.key.equals(key)) { return existingEntry.value; } existingEntry = existingEntry.next; } return null; } private int getBucketIndex(K key) { int hashCode = key.hashCode(); return Math.abs(hashCode) % buckets.length; } static class Entry { K key; V value; Entry next; public Entry(K key, V value) { this.key = key; this.value = value; this.next = null; } }}
咱们这个即为简单的版本,有两个特色:
- 解决hash碰撞,使用了 链地址法
- 将键转化为数组的索引的时候,使用 了 优化版本的 除留余数法
如果对这些基础知识不熟悉,可以看一下 尼恩给大家展示的基本原理。
哈希映射(哈希表)基本原理
为了一次存储便能得到所查记录,在记录的存储位置和它的关键字之间建立一个确定的对应关系H,已H(key)作为关键字为key的记录在表中的位置,这个对应关系H为哈希(Hash)函数, 按这个思路建立的表为哈希表。
哈希表也叫散列表。
从根本上来说,一个哈希表包含一个数组,通过特殊的关键码(也就是key)来访问数组中的元素。
哈希表的主要思想:
(1)存放Value的时候,通过一个哈希函数,通过关键码(key)进行哈希运算得到哈希值,然后得到映射的位置, 去寻找存放值的地方 ,
(2)读取Value的时候,也是通过同一个哈希函数,通过关键码(key)进行哈希运算得到哈希值,然后得到 映射的位置,从那个位置去读取。
哈希函数
哈希表的组成取决于哈希算法,也就是哈希函数的构成。
哈希函数计算过程会将键转化为数组的索引。
一个好的哈希函数至少具有两个特征:
(1)计算要足够快;
(2)最小化碰撞,即输出的哈希值尽可能不会重复。
那接下来我们就来看下几个常见的哈希函数:
直接定址法
- 取关键字或关键字的某个线性函数值为散列地址。
- 即 f(key) = key 或 f(key) = a*key + b,其中a和b为常数。
除留余数法
将整数散列最常用方法是除留余数法。除留余数法的算法实用得最多。
我们选择大小为m的数组,对于任意正整数k,计算k除以m的余数,即f(key)=k%m,f(key) 每种数据类型都需要相应的散列函数. 例如,Interge的哈希函数就是直接获取它的值: 对于字符串类型则是使用了s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]的算法: double类型则是使用位运算的方式进行哈希计算: 于是Java让所有数据类型都继承了超类Object类,并实现hashCode()方法。接下来我们看下Object.hashcode方法。Object类中的hashcode方法是一个native方法。 hashCode 方法的实现依赖于jvm,不同的jvm有不同的实现,我们看下主流的hotspot虚拟机的实现。 hotspot 定hashCode方法在src/share/vm/prims/jvm.cpp中,源码如下: 接下来我们看下ObjectSynchronizer::FastHashCode 方法是如何返回hashcode的,ObjectSynchronizer::FastHashCode 在synchronized.hpp文件中, 关于对象头、java内置锁的内容请阅读《Java 高并发核心编程 卷2 加强版》。 ObjectSynchronizer :: FastHashCode()也是通过调用identity_hash_value_for方法返回值的,调用了get_next_hash()方法生成hash值,源码如下: 到底用的哪一种计算方式,和参数hashCode有关系,在src/share/vm/runtime/globals.hpp中配置了默认: openjdk6: openkjdk8: 也可以通过虚拟机启动参数-XX:hashCode=n来做修改。 到这里你知道hash值是如何生成的了吧。 哈希表因为其本身结构使得查找对应的值变得方便快捷,但是也带来了一些问题,问题就是无论使用哪种方式生成hash值,总有产生相同值的时候。接下来我们就来看下如何解决hash值相同的问题。 对于两个不同的数据元素通过相同哈希函数计算出来相同的哈希地址(即两不同元素通过哈希函数取模得到了同样的模值),这种现象称为哈希冲突或哈希碰撞。 一般来说,哈希冲突是无法避免的。如果要完全避免的话,那么就只能一个字典对应一个值的地址,这样一来, 空间就会增大,甚至内存溢出。减少哈希冲突的原因是Hash碰撞的概率就越小,map的存取效率就会越高。 常见的哈希冲突的解决方法有开放地址法和链地址法: 开放地址法又叫开放寻址法、开放定址法,当冲突发生时,使用某种探测算法在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。开放地址法需要的表长度要大于等于所需要存放的元素。 按照探测序列的方法,可以细分为线性探查法、平法探查法、双哈希函数探查法等。 这里为了更好的展示三种方法的效果,我们用例子来看看:设关键词序列为{47,7,29,11,9,84,54,20,30},哈希表长度为13,装载因子=9/13=0.69,哈希函数为f(key)=key%p=key%11 (1)线性探测法 当我们的所需要存放值的位置被占了,我们就往后面一直加1并对m取模直到存在一个空余的地址供我们存放值,取模是为了保证找到的位置在0~m-1的有效空间之中。 公式:fi=(f(key)+i) % m ,0 ≤ i ≤ m-1i会逐渐递增加1) 具体做法: 探查时从地址d开始,首先探查T[d],然后依次探查T[d+1]....直到T[m-1],然后又循环到T[0]、T[1],...直到探查到有空余的地址或者直到T[d-1]为止。 用线性探测法处理冲突得到的哈希表如下 缺点:需要不断处理冲突,无论是存入还是査找效率都会大大降低。 (2)平方探查法 当我们的所需要存放值的位置被占了,会前后寻找而不是单独方向的寻找。 公式:fi=(f(key)+di) % m,0 ≤ i ≤ m-1 具体操作:探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+di],di 为增量序列12、-12,22、-22, ……,q2、-q2且q≤1/2 (m-1) ,直到探查到 有空余地址或者到 T[d-1]为止。 用平方探查法处理冲突得到的哈希表如下 (3)双哈希函数探查法 公式:fi=(f(key)+i*g(key)) % m (i=1,2,……,m-1)其中f(key) 和g(key) 是两个不同的哈希函数,m为哈希表的长度。 具体步骤: 双哈希函数探测法,先用第一个函数f(key)对关键码计算哈希地址,一旦产生地址冲突,再用第二个函数 g(key)确定移动的步长因子,最后通过步长因子序列由探测函数寻找空的哈希地址。 开发地址法,通过持续的探测,最终找到空的位置。为了解决这个问题,引入了链地址法。 在哈希表每一个单元中设置链表,某个数据项对的关键字还是像通常一样映射到哈希表的单元中,而数据项本身插入到单元的链表中. 链地址法简单理解如下: 来一个相同的数据,就将它插入到单元对应的链表中,在来一个相同的,继续给链表中插入。 链地址法解决哈希冲突的例子如下: (1)采用除留余数法构造哈希函数,而 冲突解决的方法为 链地址法。 (2)具体的关键字列表为(19,14,23,01,68,20,84,27,55,11,10,79),则哈希函数为f(key)=key MOD 13。则采用除留余数法和链地址法后得到的预想结果应该为: 哈希造表完成后,进行查找时,首先是根据哈希函数找到关键字的位置链,然后在该链中进行搜索,如果存在和关键字值相同的值,则查找成功,否则若到链表尾部仍未找到,则该关键字不存在。 哈希表作为一个非常常用的查找数据结构,它能够在O(1)的时间复杂度下进行数据查找,时间主要花在计算hash值上。在Java中,典型的Hash数据结构的类是HashMap。 然而也有一些极端的情况,最坏的就是hash值全都映射在同一个地址上,这样哈希表就会退化成链表,例如下面的图片: 当hash表变成图2的情况时,查找元素的时间复杂度会变为O(n),效率瞬间低下, 所以,设计一个好的哈希表尤其重要,如HashMap在jdk1.8后引入的红黑树结构就很好的解决了这种情况。 要拿高分,写个极简的版本,是不够的。 接下来模拟JDK的HashMap,我们就自己来手写hashMap。 设计不能少,首先,尼恩给大家做点简单的设计: 宏观上来说:数组+ 链表 设计一个Table 数组来存储每个key-value对,一个key-value对封装为一个Node,其中每个Node可以增加指针,指向后继节点,可以形成一个链表结构,用于解决hash冲突问题。 设计一组方法,进行原始的 put、get。 既然接下来模拟JDK的HashMap,知己方能知彼,首先来看看,一个JDK 1.8版本ConcurrentHashMap实例的内部结构示例如图7-16所示。 图7-16 一个JDK 1.8 版本ConcurrentHashMap实例的内部结构 以上的内容,来自 尼恩的 《Java 高并发核心编程 卷2 加强版》,尼恩的高并发三部曲,很多小伙伴反馈说:相见恨晚,爱不释手。 接下来,开始定义顶层的访问接口。 首先,我们需要的是确定HashMap结构,那么咱们就定义一个Map接口和一个Map实现类HashMap,其结构如下: 在Map接口中定义了以下几个方法 定义好Map接口后,那么接下来我们就需要实现Map接口,定义实现类为HashMap。 HashMap类如下: HashMap类的构造函数中,仅对数组扩容阈值做了默认设置, 默认的数组扩容阈值等于数组默认容量*负载因子(0.75) 在hashMap中定义数组集合节点Node 在Node节点中定义了hash值,Key值,Value值和指针, 其核心代码如下: 定义好Node 数组定义如下: 使用table中存储key-value键值对的前提是获取到table的下标值,在这我们采用最常用的hash函数-除留余数法f(key) =key%m 获取散列地址作为数组table的下标值,hash()函数实现如下: 通过hash()函数计算出散列地址作为数组下标后,那么我们就可以实现Key-Value键值对的存储。 HashMap的构造函数中仅设置了数组扩容的阈值,但是并没对数组进行初始化,那么就需要在第一次保存Key-Value值时进行数组table的初始化。 hash表最常见的问题就是hash碰撞,hash碰撞的解决方法有两种,开放地址法和链地址法, 我们先用最简单的开放地址法来解决hash冲突,那么保存Key-Value键值对的具体实现如下: 在第一次调用put()方法保存Key-Value键值对的时候,调用ensureCapacity()方法初始化数组。 在保存Key-Value键值对后需要判断是否需要扩容,扩容的条件是当前数组中元素个数超过阈值就需要扩容。 调用ensureCapacity()方法进行扩容操作,每次新容量=1.5 * 数组原容量; 具体代码实现如下: 从上述代码可看到,在原数组容量超过阈值的时候,就会进行扩容操作,扩容成功后还需要做以下几件事: HashMap存储Key-Value键值对到此就完成了,我们来写一个测试单元来看下执行效果,测试单元代码如下: 执行结果如下: 存储结构如下图所示: Key-Value 键值对已经保存到数组中了,那接下来我们就来探索下在HashMap中如何通过Key值某个Value值。 主要是通过for循环遍历查找,如果hash值相同或者Key值相同就说明找到Key-Value键值对,然后返回对应的value值,具体实现如下: 用测试单元来查看Key = 19 看返回的值是否正确,测试单元如下: 执行结果如下: 从上述的HashMap 的put()方法采用的开发地址法持续探测最终找到空的位置保存Key-Value键值对,在get()方法中也是通过循环不断的探测hash值或Key值。这种方式在记录总数可以预知的情况下,可以创建完美的hash表,这种情况下存储效率是很高的。 但是在实际应用中,往往记录的数据量是不确定的,那么存储的数组元素超过阈值的时候就需要进行扩容操作,扩容操作的时间成本是很高的,频繁的扩容操作同样也会程序的性能。 采用开放地址法是通过不断的探测寻找空地址,探测的过程的时间成本也是很高的,而且在查找key-value键值对时,就不能单纯的使用数组下标的方式获取,而是通过循环的方式进行查找,这个过程也是十分消耗时间的。 针对hash表的开放地址法存在的问题,我们引入链地址法来解决, jdk1.7以及之前的HashMap就是采用的数组+链表的方式进行解决的。 首先,我们对存储Key-Value键值对的put方法进行优化,优化的内容就是把有hash碰撞的Key-Value键值对用链表的形式进行存储,采用尾插入的方式往链表中插入有hash碰撞的Key-Value键值对,具体实现如下: 执行测试单元结果如下: 存储的结构如下图所示: 保存有hash碰撞的Key-Value键值对时采用了链表形式,那么在调用get()方法查找的时候, 首先通过hash()函数计算出数组的下标索引值,然后通过下标索引值查找数组对应的Node 若不是第一个结点不是要查找的Key-Value键值对,就从头开始变量链表进行Key-Value键值查找,查找到了就返回Key-Value键值对,没有查找到就返回null, 使用链地址法优化后的get()方法实现代码如下: 采用链地址法解决hash碰撞问题相比开放地址法来说,处理冲突简单且无堆积现象, 发生hash碰撞后不用探测空位置保存元素,数组table也不需要频繁的进行扩容操作。 而且链表地址法中链表采用的时候尾插入方式增加节点,不会出现环问题,而且链表的节点插入效率比较高;链表上的节点空间是动态申请的,它更适合需要保存的Key-Value键值对个数不确定的情况,节省了空间也提高了插入效率。 但是链表不支持随机访问,查找元素效率比较低,需要遍历结点,所以当链表长度过长的时候,查找元素效率就会比较低,那么在链表长度超过一定阈值的时候,我们可以把链表转换成红黑树来提升查询的效率。 采用红黑树来提升查询效率,首先需要定义红黑树的节点,该节点继承了Node节点,同时新增了左右结点和父节点。代码如下: 接下来我们来优化一下Key-Value键值存储的put()方法,优化的点主要是Hash碰撞后的处理,具体如下: 首先,我们先定义一个链表转红黑树的阈值, 接下来我们看下put()方法的执行流程: 执行流程如下图所示: put()方法优化后的代码如下: 首先我们来看下当链表的长度大于8时,是如何把链表转换成红黑树的, 这里采用的是遍历链表,然后把链表中的节点一个个转换成功红黑树节点后,插入到红黑树中,最后做自平衡操作。 我们来看下把链表转换成红黑树的实现代码如下 链表转红黑树的时候,调用了节点插入的 putRBTreeVal()方法, 由于红黑树是二叉树的其中一种,根据二叉树的特性,左子树的值都比根结点值小,右子树的值都比根结点值大。 由于同一颗红黑树的hash值都是相同的,在插入新节点之前,那我们就需要比较Key值的大小,大的往右子树放,小的就往左子树放,那么putRBTreeVal()方法的实现如下: 虽然说红黑树不是严格的平衡二叉查找树,但是红黑树插入/移除节点后仍然需要根据红黑树的五个特性进行自平衡操作。 由于红色破坏原则的可能性最小,插入的新节点颜色默认是红色。 若红黑树还没有根结点,新插入的红黑树节点就会被设置为根结点,然后根据特性2(根节点一定是黑色)把根节点设置为黑色后返回。 若父节点是黑色的,插入节点是红色的,不会影响红黑树的平衡,所以直接插入无需做自平衡。 若插入节点的父节点为红色的,那么该父节点不可能成为根结点,就需要找到祖父节点和叔父节点,那这个时候就会出现两种状态:(1)父亲和叔叔为红色;(2)父亲为红色,叔叔为黑色。 出现这两种状态的时候就需要做自平衡操作, 如果父节点和叔父节点都是红色的话,根据红黑树的特性4(红色节点不能相连)可以推断出祖父节点肯定为黑色。那这个时候只需进行变色操作即可,把祖父节点变成红色,父节点和叔父节点变成黑色操作 若叔父节点为黑色, 父节点为红色,若新插入的红色节点在父节点的左侧,此处就出现了LL型失衡,自平衡操作就需要先进行变色,然后父节点进行右旋操作;若新插入的红色节点在父节点的右侧,此处就出现了LR型失衡,自平衡操作就需要先父节点进行左旋,将父节点设置为当前节点,然后再按LL型失衡操作进行自平衡操作即可。 若叔叔为黑节点,父亲为红色,并且父亲节点是祖父节点的右子节点,如果新插入的节点为其父节点的右子节点,此时就出现了RR型失衡操作, 自平衡处理操作是先进行变色处理,把父节点设置成黑色,把祖父节点设置为红色,然后祖父节点进行左旋操作;若新插入节点,为其父节点的左子节点,此时就出现了RL型失衡,自平衡操作是对父节点进行右旋,并将父节点设置为当前节点,接着按RR型失衡进行自平衡操作。 自平衡操作的实现代码如下: 使用单元测试看下红黑树的结果: 存储结构如下: 同样,查找Key-Value键值对的get()方法也同样需要做优化, 主要优化的内容就是在红黑树中查找Key-Value键值对; 实现步骤如下: (1)通过hash值找到数组table的下标, (2)通过数组table下标判断是否是红黑树节点,若是红黑树节点就在红黑树中查找; (3)通过数组table下标判断是否是链表节点,若是链表节点就在链表中查找; (4)若结点都不在红黑树和链表中,就在数组table中查找; 实现代码如下: 如果HashMap需要通过key值移除Key-Value键值对,首先通过key值查找到节点,然后进行移除; 若需移除的节点在红黑树中,首先需要判断移除节点的度是多少,若度为2的话,就需要先找到后继节点后才可以移除,若度为1或0的话,可以直接进行移除操作,红黑树移除节点同样也需要判断红黑树是否平衡,若不平衡就需要红黑树自平衡操作,自平衡操作和插入节点的平衡操作一样,就不在赘述了。具体代码实现如下: 最终,要想让面试官五体投地,咱们还是的熟背hashMap源码。 (hashmap 的源码剖析是jdk1.8的) HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。 HashMap 的实现不是同步的,这意味着它是线程不安全的。 它的key、value都可以为null。此外,HashMap中的映射是无序的。 Jdk1.7中HashMap的实现的基础数据结构是数组+链表,每一对key->value的键值对组成Entity类以双向链表的形式存放到这个数组中; 元素在数组中的位置由key.hashCode()的值决定,如果两个key的哈希值相等,即发生了哈希碰撞,则这两个key对应的Entity将以链表的形式存放在数组中。如下图所示 在jdk1.8及以后的版本,HashMap的实现的基础数据结构是数组+链表+红黑树; 为了提高hashmap的效率,新增了红黑树,如果链表的长度超过8,且table的容量必须大于64时,会将链表转换成红黑树。 如下图所示: 既然红黑树的效率高,为什么不直接用红黑树?为什么链表超过8转换为红黑树? 官方给出的解释如下: 这段话的意思提现了时间和空间平衡的思想。 最开始使用链表的时候,空间占用是比较少的,而且由于链表短,所以查询时间也没有太大的问题。 可是当链表越来越长,需要用红黑树的形式来保证查询的效率。 对于何时应该从链表转化为红黑树,需要确定一个阈值,这个阈值默认为 8,链表长度达到 8 就转成红黑树,而当长度降到 6 就转换回链表。 如果 hashCode 分布良好,也就是hash计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。 在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为8的时候,概率仅为0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。 事实上,链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。 除了jdk1.8中新增了红黑树外,从jdk1.8开始,链表节点的插入使用尾插入替换了jdk1.7的头插入,替换的原因是在并发情况下,头插法会出现链表成环的问题, 为了利用数组索引进行快速查找,hashMap采取hash算法的是先将 key值映射成数组下标。 hash()算法的源码如下: 从源码中可以看到,没有直接使用hashCode返回hash值,是因为hashCode返回的是int值,它的范围是在-2147483648-2147483647。如果存的元素并不多的情况,创建int范围的的数组空间太过于浪费。 hash()分为两个步骤: ①先得到扰动后的key的hashCode:(h = key.hashCode() )^ (h >>> 16) 首先 h = key.hashCode()是key对象的一个hashCode,每个不同的对象其哈希值都不相同,其实底层是对象的内存地址的散列值,所以最开始的h是key对应的一个整数类型的哈希值;右移16位(h>>>16),然后高位补0是为了让高16位参与进来。 ②再将hashCode映射成有限的数组下标index:(n - 1) & hash; 采用异或(^)运算是为了让h的低16位更有散列性。 为什么异或运算的散列性更好呢?我们来看组运算例子; 上面的计算过程如下: 与运算:其中1&1=1,其他三种情况1&0=0, 0&0=0, 0&1=0 都等于0,可以看到与运算的结果更多趋向于0,这种散列效果就不好了,运算结果会比较集中在小的值 或运算:其中0&0=0,其他三种情况 1&0=1, 1&1=1, 0&1=1 都等于1,可以看到或运算的结果更多趋向于1,散列效果也不好,运算结果会比较集中在大的值 异或运算:其中0&0=0, 1&1=0,而另外0&1=1, 1&0=1 ,可以看到异或运算结果等于1和0的概率是一样的,这种运算结果出来当然就比较分散均匀了 总的来说,与运算的结果趋向于得到小的值,或运算的结果趋向于得到大的值,异或运算的结果大小值比较均匀分散,这就是我们想要的结果。 右移16位,然后再与原hashcode做异或运算,是为了高低位二进制特征混合起来,使该hashCode映射成数组下标时可以更均匀。更好地均匀散列,从而减少碰撞,进一步降低hash冲突的几率。 所以计算的过程如下: 这部分产生的hash值是h,这个数有可能很大,不能直接拿来当数组下标,那么接下来就需要进行第二部分的内容(n - 1) & hash,这部分内容就是hashMap中获取数组下标的代码,n=table.length 新数组长度。 hash是参数h(上一步计算返回的hash结果)。 HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同。想到的办法就是取模运算:hash%length,但是在计算机中取模运算效率与远不如位移运算(&)高。主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。所以官方决定采用使用位运算(&)来实现取模运算(%),也就是源码中优化为:hash&(length-1)。 (n - 1) & hash的计算过程如下: 假设数组长度为16,经过(h = key.hashCode() )^ (h >>> 16)得到的hash值的低16位是1101001010100110(一般数hashmap数组长度都在2^16范围内,所以就用低16位演示了); 首先,n-1 = 15,转换成二进制是1111.然后与hash值进行与运算(当两个数字对应的二进位均为1时,结果位为1,否则为0。参与运算的数以补码出现).计算过程如下: 结果是0000,换算成十进制就是0,对应的数组下标就是0; 这样两步就完成了key对象映射到指定数组索引上了。 哈希桶数组的大小, 在空间成本和时间成本之间权衡,时间和空间之间进行权衡: 其实就是在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。 在剖析table扩容之前,我们先来了解hashMap中几个比较重要的属性。 HashMap中有两个比较重要的属性:加载因子(loadFactor)和边界值(threshold),在HashMap时,就会涉及到这两个关键初始化参数,loadFactor和threshold的源码如下: Node[] table的初始化长度length(默认值是16),length大小必须为2的n次方,主要是为了方便扩容。 loadFactor 为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node 个数。threshold 、length 、loadFactor 三者之间的关系: threshold = length * Load factor 默认情况下 threshold = 16 * 0.75 =12。 threshold就是允许的哈希数组最大元素数目,超过这个数目就重新resize(扩容),扩容后的哈希数组 容量length 是之前容量length 的两倍。 threshold是通过初始容量和LoadFactor计算所得,在初始HashMap不设置参数的情况下,默认边界值为12。 如果HashMap中Node的数量超过边界值,HashMap就会调用resize()方法重新分配table数组。这将会导致HashMap的数组复制,迁移到另一块内存中去,从而影响HashMap的效率。 loadFactor默认值是0.75,官方给的解释如下: 大概意思是:作为一般规则,默认负载因子 (.75) 在时间和空间成本之间提供了良好的折衷。较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括get和put )。在设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以尽量减少重新哈希操作的次数。 如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。 loadFactor 也是可以调整的,建议大家尽量不要修改,除非在时间和空间比较特殊的情况: 接下来我们再来看一个size属性。 size属性是HashMap中实际存在的键值对数量;而length是哈希桶数组table的长度。 当HashMap中的元素越来越多的时候,碰撞的几率也就越来越高,所以为了提高查询的效率,就要对HashMap的数组进行扩容,其实数组扩容这个操作在ArrayList中也出现了,所以这是一个通用的操作, table是一个Node Node 类作为 HashMap 中的一个内部类,除了 key、value 两个属性外,还定义了一个next 指针,当有哈希冲突时,HashMap 会用之前数组当中相同哈希值对应存储的 Node 对象,通过指针指向新增的相同哈希值的Node 对象的引用。 table在首次使用put的时候初始化,并根据需求调整大小。 当table中的Node JDK7 中的扩容机制 (1)空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数组。 (2)有参构造函数:根据参数确定容量、负载因子、阈值等。 (3)第一次 put 时会初始化数组,其容量变为不小于指定容量的 2 的幂数,然后根据负载因子确定阈值。 (4)如果不是第一次扩容,则 新容量=旧容量 x 2 ,新阈值=新容量 x 负载因子 。 JDK8 的扩容机制 (1)空参数的构造函数:实例化的 HashMap 默认内部数组是 null,即没有实例化。第一次调用 put 方法时,则会开始第一次初始化扩容,长度为 16。 (2)有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的 2 的幂数, 哈希桶数组table的扩容核心是resize()方法。在resize的时候会将原来的数组rehash重新计算hash值转移到新数组上。在HashMap数组扩容之后,最消耗性能的点是原数组中的数据必须重新计算其在新数组中的位置,并放进去。 resize()方法扩容流程如下: 那接下来我们就来看下resize()方法中是如何初始化table数组和table扩容的。源码如下: 从源码中我们知道,默认的数组长度length 是16.这个主要是为了实现均匀分布。因为在使用2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。 table的threshold 阈值是通过初始容量和 loadFactor计算所得,在初始HashMap 不设置参数的情况下,默认边界值为12(160.75)。当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置。 table的扩容分为两步: 第一步:扩容——创建一个新的Entry空数组,长度是原数组的2倍。 第二步:ReHash——遍历原Entry数组,把所有的Entry重新Hash到新数组。 扩容的后重新计算hash的原因是因为长度扩大以后,Hash的规则也随之改变。 首先我们来看下put(K key, V value)的源码下: 从源码可以看到,put()方法首先调用hash()算法计算hash值,然后调用putVal()对添加的key-value键值对进行存储。 在putVal()中主要完成了一下几件事: (1)如果发现当前的桶数组为null,则调用resize()方法进行初始化 (2)如果没有发生哈希碰撞,则直接放到对应的桶中 (3)如果发生哈希碰撞,且节点已经存在,就替换掉相应的value (4)如果发生哈希碰撞,且桶中存放的是树状结构,则挂载到树上 (5)如果碰撞后为链表,添加到链表尾,如果链表长度超过TREEIFY_THRESHOLD默认是8,则将链表转换为树结构 (6)数据put完成后,如果HashMap的总数超过threshold就要resize putVal的执行流程如下: 我们来看下添加树节点的方法putTreeVal()的源码; 那接下来我们来看下是如何把节点添加到红黑树上的,调用的是putTreeVal()方法,源码如下: 在HashMap的红黑树中不是直接以key作为排序关键字来判断key的大小,而是以key的hash值作为排序的关键字来判断key的大小;当key的hash值相同时(hash 冲突),有2大类情况: (1)key实现了Comparable接口,比较key大小,决定搜索分支; (2)key没有实现Comparable接口,没法直接比较key大小,因此会搜索当前节点的左右分支; putTreeVal()方法调用了find()方法从左右子树搜寻Key,find()源码实现如下: 红黑树插入新节点后,会出现不平衡的情况,在putTreeVal中调用了balanceInsertion()方法平衡红黑树,关于红黑树的如何平衡的可参考前文。balanceInsertion()源码如下: 看完红黑树节点的插入,接下来我们来看下hashMap是如何把链表转换成红黑树的, 核心方法是treeify()方法,调用treeify()方法的是treeifyBin()方法,当链表的长度超过8的时候,就会调用treeifyBin()方法链表转化为以树节点存在的双向链表。treeifyBin()源码如下: treeify()将该双向链表转换为红黑树结构,源码如下: 以上代码就是hashmap 基于数组+链表+红黑树实现的Key-Value键值对的存储,最后用一种流程图总结一下put()方法: 接下来我们来看下删除key-value键值对。hashMap的删除方法是remove(Object key)方法,执行流程如下: 源码如下: 如果节点在红黑树中,就需要到树中进行删除,调用removeTreeNode()方法删除,源码如下: 当 HashMap 只存在数组,而数组中没有Node链表时,是HashMap查询数据性能最好的时候。 一旦发生大量的哈希冲突,就会产生 Node 链表,这个时候每次查询元素都可能遍历 Node 链表,从而降低查询数据的性能。 特别是在链表长度过长的情况下,性能明显下降,使用红黑树就很好地解决了这个问题,红黑树使得查询的平均复杂度降低到了O(log(n)),链表越长,使用红黑树替换后的查询效率提升就越明显。 get(Object key)方法执行流程如下: get(Object key)源码如下: HashMap使用了数组+链表+红黑树三种数据结构相互结合的形式存储键值对,提升了查询键值对的效率。 关于红黑树的知识,非常重要,尼恩专门写了一篇长长的文章,也是结合面试题写的,作为尼恩Java面试宝典的 专题33。 该PDF的名字为: 《尼恩Java面试宝典专题33:BST、AVL、RBT红黑树、三大核心数据结构(卷王专供+ 史上最全 + 2023面试必备》 很多小伙伴评价,通过此文,终于搞懂了红黑树。 建议大家去看看。 最后总结一下本文: 本文通过首先hashmap的学习,重点是要学会hashMap的思想,在实际开发中如何去引用hashMap的思路去解决问题,如何更好的使用HashMap,优化HashMap的性能。 尼恩提示:要想拿高薪,首先hashmap、手写 线程池,都是必须课哈。 本文1作: 唐欢,资深架构师, 《Java 高并发核心编程 加强版》作者之1 。 本文2作: 尼恩,40岁资深老架构师, 《Java 高并发核心编程 加强版 卷1、卷2、卷3》创世作者, 著名博主 。 《K8S学习圣经》《Docker学习圣经》等11个PDF 圣经的作者。 清华大学出版社《Java高并发核心编程 卷2 加强版》 《尼恩Java面试宝典专题33:BST、AVL、RBT红黑树、三大核心数据结构(卷王专供+ 史上最全 + 2023面试必备》 《吃透8图1模板,人人可以做架构》 《10Wqps评论中台,如何架构?B站是这么做的!!!》 《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》 《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》 《100亿级订单怎么调度,来一个大厂的极品方案》 《2个大厂 100亿级 超大流量 红包 架构方案》 … 更多架构文章,正在添加中 《响应式圣经:10W字,实现Spring响应式编程自由》 这是老版本 《Flux、Mono、Reactor 实战(史上最全)》 《Spring cloud Alibaba 学习圣经》 PDF 《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》 《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》 《Linux命令大全:2W多字,一次实现Linux自由》 《TCP协议详解 (史上最全)》 《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》 《Redis分布式锁(图解 - 秒懂 - 史上最全)》 《Zookeeper 分布式锁 - 图解 - 秒懂》 《队列之王: Disruptor 原理、架构、源码 一文穿透》 《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》 《缓存之王:Caffeine 的使用(史上最全)》 《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》 4000页《尼恩Java面试宝典 》 40个专题
关键词:
5注册-控制层5 1创建响应状态码、状态码描述信息、数据。这部分功能封装到一个类中,将这类作为方法返回值 前言:自从接触异步(asyncawaitTask)操作后,始终都不明白,这个Task调度的问题。接触Quartz net已经很久数字分析法
平方取中法
随机数法
public static int hashCode(int value) { return value;}
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { hash = h = isLatin1() ? StringLatin1.hashCode(value) : StringUTF16.hashCode(value); } return h;}public static int hashCode(byte[] value) { int h = 0; for (byte v : value) { h = 31 * h + (v & 0xff); } return h;}public static int hashCode(byte[] value) { int h = 0; int length = value.length >> 1; for (int i = 0; i < length; i++) { h = 31 * h + getChar(value, i); } return h;}
public int hashCode() { long bits = doubleToLongBits(value); return (int)(bits ^ (bits >>> 32));}public static long doubleToLongBits(double value) { long result = doubleToRawLongBits(value); if ( ((result & DoubleConsts.EXP_BIT_MASK) == DoubleConsts.EXP_BIT_MASK) && (result & DoubleConsts.SIGNIF_BIT_MASK) != 0L) result = 0x7ff8000000000000L; return result;}
public native int hashCode();
JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle)) JVMWrapper("JVM_IHashCode"); return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;JVM_END
intptr_t ObjectSynchronizer::identity_hash_value_for(Handle obj) { return FastHashCode (Thread::current(), obj()) ;}intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) { if (UseBiasedLocking) { if (obj->mark()->has_bias_pattern()) { // Box and unbox the raw reference just in case we cause a STW safepoint. Handle hobj (Self, obj) ; // Relaxing assertion for bug 6320749. assert (Universe::verify_in_progress() || !SafepointSynchronize::is_at_safepoint(), "biases should not be seen by VM thread here"); BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current()); obj = hobj() ; assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } } ObjectMonitor* monitor = NULL; markOop temp, test; intptr_t hash; // 获取调用hashCode() 方法的对象的对象头中的mark word markOop mark = ReadStableMark (obj); // object should remain ineligible for biased locking assert (!mark->has_bias_pattern(), "invariant") ; if (mark->is_neutral()) { //普通对象 hash = mark->hash(); // this is a normal header //如果mark word 中已经保存哈希值,那么就直接返回该哈希值 if (hash) { // if it has hash, just return it return hash; } // 如果mark word 中还不存在哈希值,那就调用get_next_hash(Self, obj)方法计算该对象的哈希值 hash = get_next_hash(Self, obj); // allocate a new hash code // 将计算的哈希值CAS保存到对象头的mark word中对应的bit位,成功则返回,失败的话可能有几下几种情形: //(1)、其他线程也在install the hash并且先于当前线程成功,进入下一轮while获取哈希即可 //(2)、有可能当前对象作为监视器升级成了轻量级锁或重量级锁,进入下一轮while走其他case; temp = mark->copy_set_hash(hash); // merge the hash code into header // use (machine word version) atomic operation to install the hash test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark); if (test == mark) { return hash; } // If atomic operation failed, we must inflate the header // into heavy weight monitor. We could add more code here // for fast path, but it does not worth the complexity. } else if (mark->has_monitor()) { //重量级锁 // 果对象是一个重量级锁monitor,那对象头中的mark word保存的是指向ObjectMonitor的指针, //此时对象非加锁状态下的mark word保存在ObjectMonitor中,到ObjectMonitor中去拿对象的默认哈希值: monitor = mark->monitor(); temp = monitor->header(); assert (temp->is_neutral(), "invariant") ; hash = temp->hash(); //(1)如果已经有默认哈希值,则直接返回; if (hash) { return hash; } // Skip to the following code to reduce code size } else if (Self->is_lock_owned((address)mark->locker())) { //轻量级锁锁 //如果对象是轻量级锁状态并且当前线程持有锁,那就从当前线程栈中取出mark word: temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned assert (temp->is_neutral(), "invariant") ; hash = temp->hash(); // by current thread, check if the displaced //(1)如果已经有默认哈希值,则直接返回; if (hash) { // header contains hash code return hash; } } // Inflate the monitor to set hash code monitor = ObjectSynchronizer::inflate(Self, obj); // Load displaced header and check it has hash code mark = monitor->header(); assert (mark->is_neutral(), "invariant") ; hash = mark->hash(); //计算默认哈希值并保存到mark word中后再返回 if (hash == 0) { hash = get_next_hash(Self, obj); temp = mark->copy_set_hash(hash); // merge hash code into header assert (temp->is_neutral(), "invariant") ; test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark); if (test != mark) { hash = test->hash(); assert (test->is_neutral(), "invariant") ; assert (hash != 0, "Trivial unexpected object/monitor header usage."); } } // We finally get the hash return hash;}
static inline intptr_t get_next_hash(Thread * Self, oop obj) { intptr_t value = 0 ; if (hashCode == 0) { //随机数 openjdk6、openjdk7 采用的是这种方式 // This form uses an unguarded global Park-Miller RNG, // so it"s possible for two threads to race and generate the same RNG. // On MP system we"ll have lots of RW access to a global, so the // mechanism induces lots of coherency traffic. value = os::random() ; } else if (hashCode == 1) { //基于对象内存地址的函数 // This variation has the property of being stable (idempotent) // between STW operations. This can be useful in some of the 1-0 // synchronization schemes. intptr_t addrBits = cast_from_oop
product(intx, hashCode, 0, \ "(Unstable) select hashCode generation algorithm") \
product(intx, hashCode, 5, \ "(Unstable) select hashCode generation algorithm") \
hash 碰撞(哈希冲突)
开放地址法
关键词(key) 47 7 29 11 9 84 54 20 30 散列地址k(key) 3 7 7 0 9 7 10 9 8 链地址法
手写一个相对复杂的HashMap
复杂的HashMap的数据模型设计+接口设计
定义顶层的访问接口
Map接口的实现
public interface Map
手写实现类HashMap
public class HashMap
Node
/*** 链表结点** @param
// 数组Node
hash()函数实现
private int hash(Object key) { if (key == null) return 0; int hash = (Integer) key % 4; return hash;}
开放地址法解决hash碰撞
/** * 插入节点 * * @param key key值 * @param value value值 * @return */@Overridepublic V put(K key, V value) { //通过key计算hash值 int hash = hash(key); //数组 Node
/** * 数组扩容 */private Node
@Testpublic void hashMapTest() { HashMap
@Overridepublic V get(Object key) { Node
@Testpublic void hashMapTest01() { HashMap
链地址法解决hash碰撞
/** * 插入节点 * * @param key key值 * @param value value值 * @return */@Overridepublic V put(K key, V value) { ... // 开始时插入元素 if ((parent = tab[i = hash]) == null) { System.out.println("下标为:"+i+"数组插入的key:" + key + ",value:" + value); //如果没有hash碰撞,就直接插入数组中 tab[i] = new Node<>(hash, key, value, null); ++size; } else { //有哈希碰撞时,采用链表存储 // 下一个子结点 Node
@Overridepublic V get(Object key) { Node
红黑树提升查询效率
/** * 红黑树结点 * * @param
//链表长度到达8时转成红黑树private static final int TREEIFY_THRESHOLD = 8;
/** * 插入节点 * * @param key key值 * @param value value值 * @return */@Overridepublic V put(K key, V value) { //通过key计算hash值 int hash = hash(key); //数组 Node
/** * 把链表转换成红黑树 * * @param tab * @param hash */ private void linkToRBTree(Node
RBTreeNode
/** * 添加后平衡二叉树并设置结点颜色 * * @param node 新添结点 * @param hash hash值 */private void fixAfterPut(RBTreeNode
@Overridepublic V get(Object key) { Node
/** * 结点删除 * * @param key * @return */@Overridepublic V remove(K key) { int hash = hash(key); //数组 Node
熟背JDK的hashMap源码剖析
数组下标获取
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
table桶的扩容机制
final float loadFactor;int threshold;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
transient Node
static class Node
final Node
put(K key, V value) 添加key-value
public V put(K key, V value) { //返回putVal方法, 给key进行了一次rehash return putVal(hash(key), key, value, false, true);}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab:引用hashMap的散列表 //p:表示当前散列表的元素 // n :表示散列表数组的长度 //i:表示路由寻址的结果 Node
final TreeNode
/** * 从左右子树搜寻K * K 搜索目标 * h 目标key的hash值 * kc key的class对象 */final TreeNode
/** * 红黑树添加平衡 * @param root * @param x * @param
final void treeifyBin(Node
final void treeify(Node
remove() 删除Key-Value
public V remove(Object key) { Node
/** * 这个方法是HashMap.TreeNode的内部方法,调用该方法的节点为待删除节点 * * @param map 删除操作的map * @param tab map存放数据的链表 * @param movable 是否移动跟节点到头节点 */final void removeTreeNode(HashMap
get(Object key)方法
public V get(Object key) { Node
关于红黑树
作者介绍:
参考文献:
技术自由的实现路径:
实现你的 架构自由:
实现你的 响应式 自由:
实现你的 spring cloud 自由:
实现你的 linux 自由:
实现你的 网络 自由:
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
实现你的 面试题 自由:
免费获取11个技术圣经PDF:
【世界热闻】SpringBoot+MyBatis+MySQL电脑商城项目实战(四)用户注册—控制层
自定义一个简单的Task调度器、任务循环调度器、TaskScheduler
京东太猛,手写hashmap又一次重现江湖-全球今日报
【世界热闻】SpringBoot+MyBatis+MySQL电脑商城项目实战(四)用户注册—控制层
自定义一个简单的Task调度器、任务循环调度器、TaskScheduler
货币市场日报:5月17日
世界要闻:苹果WWDC来了!iOS 17有三大变化
新消息丨葡萄健康栽培与病虫害防控(关于葡萄健康栽培与病虫害防控的简介)
每日快看:318川藏线巨石滚落砸烂一轿车:车内人员躲过一劫
荣耀90 Pro真机曝光:“星钻银”配色耀眼 灵感来自珠宝王冠-环球快看点
马斯克称特斯拉电动皮卡将在今年开始交付 不打算卸任CEO-最新资讯
比用毛巾还便宜 不怕有味儿:大牌洗脸巾7.9元100抽狂促
美国教授用ChatGPT判定学生论文抄袭:结果尴尬!聪明反被聪明误|今日热门
天天快资讯:时隔3年再“发车”,“北斗专列”如何升级?
【Linux】详解Centos7的下载安装配置
焦点日报:【财经分析】多重因素影响REITs市场表现 短期调整无碍机构长期看多
概念动态|机器人新增“比亚迪概念”
世界球精选!你会买吗?澳航推“邻座无人”服务 最低仅140元
十年果粉换OPPO Find X6 Pro后直呼惊艳:果断把iPhone 14 Pro挂闲鱼卖掉|世界聚看点
视讯!拳头性别歧视案尘埃落定 将赔偿每位女性最多15万刀
男子为稳坐榜一大哥骗取乙方百万:全部打赏给女主播 被判刑
世界报道:巴西貘被饲养员挠痒一脸舒适 网友:长得东拼西凑但依然很萌
上海市首届中青年工程师创新创业大赛启动
债市日报:5月17日
每日机构分析:5月17日 环球焦点
小米发布米家空调巨省电2匹:新一级能效 一年省380度电
【天天速看料】孟羽童接私活被开除 专家称老板不能要求员工没副业
全球微头条丨PC已死?联想不同意!
重新打趴中国厂商 韩国不服输:显示面板要夺回第一|天天时快讯
最后的低价?2TB三星980 Pro最低959到手
斗罗大陆冰凤凰vs火凤凰(斗罗大陆冰蝶舞)
springCloud Alibaba服务的注册与发现之eureka客户端注册-每日快看
最佳软件测试基础入门教程3软件开发生命周期的测试
黄牛为什么能抢走“五月天”的门票?|当前速讯
天天关注:记录--vue3优雅的使用element-plus的dialog
上映首日!《速度与激情10》票房突破8000万-全球看点
消息称58同城开启大裁员:裁撤比例50%以上-全球快讯
过去一年 腾讯员工减少1万人 平均工资又涨了 世界热推荐
国产龙芯3B600八个大小核、自研GPU:性能媲美Zen2、10代酷睿-当前播报
世界微动态丨你看微信视频号么?腾讯:粉丝破万创作者是去年三倍
天天视点!欧洲“核联盟”为欧洲核能发展拟定“路线图”
软件测试精品书籍文档下载
热议:4大特性看Huawei Cloud EulerOS为开发者带来平滑迁移体验
设计软件的二次开发总结(表格)
环球热门:金智科技接待中泰证券等多家机构调研
【时快讯】乌兹别克斯坦官员:乌兹别克斯坦愿与中国展开全方位各领域合作
被要求停服!CyGames《赛马娘》遭专利起诉:国服B站代理-每日速讯
RTX 4060 Ti/RX 7600新卡同时来了!NVIDIA、AMD拼价格:玩家血赚|每日视点
资讯:3年10倍!插电车型翻倍式爆卖 比亚迪独占世界4成
比5G好用10倍!北京首个5.5G实验基站正式开通
开除软件部门全体高管后 要用华为车机软件?大众回应
国家一级保护动物黄嘴白鹭“组团”造访厦门|世界快消息
kasini3000新增:ansible like输出 观焦点
小程序安全架构分析
天天微头条丨Natasha相关辅助类 (六)
SpringBoot项目预加载数据——ApplicationRunner、CommandLineRunner、InitializingBean 、@PostCo
DQL语句(一) -----简单select查询
过氧化苯甲酸叔丁酯商品报价动态(2023-05-17)
收评:沪指跌0.21%量能创逾两月新低 军工板块表现活跃
春秋时期青铜器酷似5根天线路由器 用途至今成谜 环球热头条
今日国内油价迎年内最大降幅 下次调整或将持续下跌
世界快资讯丨4年磨一剑 华为麒麟A2芯片来了:消息称已具备量产能力
全球看热讯:不负等待 新款华为MateBook E 二合一笔记本或将开启移动生产力融合时代
奥迪前CEO承认“大众排放门”存欺诈 拿839万元换缓刑
海上航行26天,首单到货!以人民币结算
SRE Google 运维解密读书笔记一:SRE 方法论概述
Python从零到壹丨带你了解图像直方图理论知识和绘制实现
匠心精神--来看一个小迭代的代码实现
《塞尔达传说:王国之泪》卖爆背后是手游行业的悲哀-全球视讯
再见吧!特斯拉强制单踏板模式
全球今亮点!“感染”塞尔达病毒后 我每天只睡三小时
硬派越野车不适合城市?仰望U8云辇-P出手:三级可调、软硬随心
ChatGPT估值已上2000亿 创始人对钱没兴趣:收入只够交保险 当前速看
23中国中药SCP001今日发布发行公告|当前聚焦
全球头条:1美金等于多少人民币元(2023年5月17日)
国内又现被驱赶的5G基站:你敢建?我就敢拆!居民称辐射大有害健康|全球热点评
开除软件部门全体高管后 大众被曝要用华为车机软件|全球微资讯
1TB硬盘只要200元 三星等带头减产闪存 国产存储公司回应:好消息 天天短讯
全球观焦点:企业级项目模板的配置与集成(Vite + Vue3 + TypeScript)
智能家居生态迎来超强辅助 快资讯
当前要闻:沈阳金融商贸开发区聘请十余位顾问委员会特邀专家
四大运营商宣布:我国启动全球首个5G异网漫游试商用
国产武侠游戏巅峰 等了16年的《仙剑4重制版》悬了
OPPO Reno10系列正面颜王:对称双曲面、2.12mm窄下巴 世界快讯
前苹果工程师被指控盗窃自动驾驶技术 为中国汽车公司牟利
ZV-1继承者来了!索尼新款Vlog相机5月23日发布
我国成功发射第五十六颗北斗导航卫星 全球观点
数据结构-环球速递
【世界热闻】提高数据的安全性和可控性,数栈基于 Ranger 实现的 Spark SQL 权限控制实践之路
环球看点!Django authenticate() 函数查找不到与提交的用户名和密码匹配的用户,则会返回 None。
apb uart IP使用说明
微软反驳马斯克:我们并没有控制OpenAI
频繁翻车、流量不再 为何明星代言手机越来越少了?
环球要闻:20年来重大转变 马斯克将花钱为特斯拉打广告
边开边充!瑞典率先打造世界首条永久性充电公路 头条
病人被医生遗忘在磁共振舱近三小时 属重大医疗事故 科普:没辐射
全球新动态:【道德经】五十·出生入死
NineData:高效高质量的 Redis 可视化管理工具|今热点
如何在不改变图片分辨率的情况下增加图片的大小
今日讯!国家发展改革委与标普评级公司召开座谈会 开展我国主权信用评级复评工作
最新70城房价出炉,4月份郑州新房同比增长0.2%
孟羽童图文广告报价至少15万一条 网友称其已赚近300万-世界聚看点