关于JDK1.7与1.8的HashMap源码解读的一些总结

本文详细解析了JDK1.7和1.8中HashMap的实现原理,包括其内部结构、put操作、扩容条件以及线程安全性问题。在JDK1.7中,HashMap使用头插法,当哈希冲突时形成链表;在JDK1.8中引入了红黑树,当链表长度超过8时转为红黑树,以提高查找效率。此外,文章还讨论了扩容策略及其对性能的影响,以及线程不安全导致的循环死链问题。
摘要由CSDN通过智能技术生成

一、jdk1.7的HashMap

HashMap底层是个Entry<K,V>数组。内部类Entry<K,V>

1、索引

不同于ArrayList可以直接传入索引index,为了保证get查询与put插入的效率,我们根据Key的hashcode去确定存储的索引,基本上都是“hashcode&table.length-1”这种思想。

2、结构

当出现哈希冲突的时候,将同一个桶内的元素用链表的方式进行组织,这就是它的实现结构“数组+链表”。

3、Jdk7采用头插法

一方面头插法(插头后直接将table[index]指向新结点)的效率高,否则需要遍历到链表,到队尾插入;另一方面我们也认为刚存入的元素,有较大可能再次被使用。

注:虽然每次插入也需要“遍历”去检查是否有相同的Key进行覆盖替换,但我们只针对插入的的这个动作分析,或者说,这种“遍历”可以在检查成功的情况下就返回,有最好与最坏情况的存在,依然要比尾插法的效率好。

*创建一个Entry结点。这段代码我们可以看出1.7HashMap的主要结构。

*了解:数组大小capacity默认16,负载因子loadfactor默认0.75。

但数组的大小并不是capacity,而是去找大于等于capacity的二次幂(这设计同用“与”运算计算index索引有关,主要是hashcode&table.length-1逻辑上是进行了“掩码”操作,所以需要table.length是2的幂次方)

4、put实现

第一次put的时候,检查到table是null,空数组会扩容,初始化;hashmap的key是支持null的,调用putForNullKey(value),否则indexfor(hash,table.length)用key的哈希值与map的长度进行求索引的操作,hash求key的哈希值的原则是key的hashcode尽量让key所有位参与(右移+亦或。1.8的这步就没有那么复杂),让哈希值更加散列,让哈希值的结果平均!

然后会有一个循环遍历该桶的结点。为什么呢?

我们看看这个运行结果

发现:key要唯一,不唯一会覆盖旧值,并且返回旧值“2”

循环就是实现这种功能,判断key值是否存在相同。若存在,就把旧值覆盖后,返回旧值。

?既然都要遍历链表,那为什么不干脆“尾插法”?

说白了他还是比尾插法快,因为我遍历到有重复的时候,其实就返回了,可能只需要遍历一小部分,并不需要一定要到尾部。

5、扩容条件

当table==null,需要初始化,会进行扩容。

if(size>Threshold = capacity * loadfactor&&新加入的元素占据新的桶),只有同时满足这两个条件才会触发扩容。

解释条件意思:

1、 存放新值的时候当前已有元素的个数必须大于等于阈值

2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)

注意:JDK8开始就仅有第一个条件了。扩容会将旧length*2作为newCapacity,但会有MAX_CAPACITY的限制。

这里可以看出扩容规则:新长度=旧长度*2,并且进行数组元素的迁移

注意:扩容以后的索引,要不就是“以前的index”,要不就是“以前的index加上旧capacity的和”,因此rehash返回的是布尔型变量。initHashSeedAsNeeded方法不展开

6、循环死链

说白了是因为hashmap线程不安全,扩容的时候,若一个线程正常进行。进行数组复制的时候,因为头插法的原因破坏了相邻两个结点的前后关系,导致另一个线程再去遍历单链表进行复制的时候,形成循环链表,陷入死循环

*线程不安全导致transfer有严重问题!

草图

*那可以不扩容吗?为什么要扩容呢?

扩容并不是单纯的增加数组长度,而是为了将链表变短。

了解:HashMap之所以在别的地方不会发生线程不安全危害,是因为采用了快速失败的容错机制,有个计数器modcount记录添加put与删除remove的次数。语法糖其实是获取了一个iterator迭代器,初始化迭代器初始化的时候会将expectcountmodcount置为相同值,因此之后再有别的线程进行删除,会modcount++,别的线程会检查到modcount!=expextcount,会抛出异常

示例代码:

等价于:

二、jdk1.8的HashMap

*知识普及:

*1、红黑树结点:left、right、parent、red

*2、关系:二叉查找树(退化单链表)—》平衡二叉树(1、复杂2、结构易破坏)—》红黑树(左中右、根叶黑、不红红、黑路同

*3、把握要点:1、新插入结点是默认红,至少这样不会破坏黑路同的特性

  1. 规则的破坏主要看黑结点是否符合规则,来进行调整

*4、手法:

父黑:直接加就行

父红:看叔

        叔红——》叔父爷都要变色,爷做为新结点向上再分析

        叔黑——》旋转,父爷变色

1、结构

hashmap最大的变化——“数组+链表+红黑树

2、put

put的时候需要判断桶的Node的结点是’单链表’还是’红黑树’,用桶上的结点类型判断,若e instanceof TreeNode == true 说明是’红黑树’

结点数大于8时,会触发树化

3、扩容触发条件

        刚开始table==null也会触发扩容。

        后面,当size数量大于阈值时,会主动触发扩容。条件比1.7中少了一条“tab[index]!=null”

4、关于扩容的rehash:

        jdk1.8的hashmap进行扩容的重新hash仍然遵循“要么是原index,要么是index+旧的长度”,hashcode&旧length高位要么是0要么是1,但数组复制的过程,在形式上有变化,1.7是循环链表一个一个移的,1.8是用两条叫做“高/低位链表”组织好,整体移动的(有点像我们玩的蜘蛛纸牌的感觉)。

        但对于jdk1.8HashMap,无论是’红黑树’还是’链表结构’都可以这么操作,唯一不同的是,红黑树遍历的时候还要记录两条链表的结点个数,根据结点数来决定是否需要untreeify进行退化(因为本身都是TreeNode)。

对两个链表的判断(针对loHead分析,hiHead也是一样的):

        情况1:低位链表存在,并且没有超过阈值,解除树化

        情况2:低位链表存在,并且超过阈值,同时高位链表存在,那么将低位链表移过去还要树化

        情况3:低位链表存在,并且超过阈值,同时高位链表还不存在,那么直接把以前的树移过去                        就好了

*补充说明:源码中的两条链表就是以loHead、hiHead为头结点

TreeNode继承自Node,增加了left、right、red成员变量,红黑树的插入是基于key的hashcode的,这就是所谓的“树化”“双向链表转红黑树”,但结点与结点之间,仍然维护着prev与next关系的双向链表。

须知:大于8时会进行单链表转红黑树,小于等于6时会退化成单链表,留一点冗余空间,是为了防止数量在8附近徘徊,造成反复转结构的开销。

?小于等于6时会退化成单链表?

红黑树退化为单链表有两种情况:

(1)remove的结点时候,检查是否需要退化,并不是用“- -size<=6”

 但很明显,针对上述条件,当出现这种情况时,size=5 < 6,仍然不会进入这个if语句内!所以小于等于6不一定触发扩容!

(2)在扩容时,若红黑树split后统计的结点数量小于等于6,会解除树化,这点与我们想的一样。

个人解释:相比于因为remove一两个结点而导致的结点数<=6,是可以容忍的,因为只不过是在临界值+1-1的小范围无意义波动,没必要去因此更改数据结构,相反,认为红黑树转单链表这种事情,发生在数组扩容造成的结点数量<=6,觉得更加有意义

*单链表转红黑树的代码

*稿图(可忽略)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值