一、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迭代器,初始化迭代器初始化的时候会将expectcount与modcount置为相同值,因此之后再有别的线程进行删除,会modcount++,别的线程会检查到modcount!=expextcount,会抛出异常
示例代码:
等价于:
二、jdk1.8的HashMap
*知识普及:
*1、红黑树结点:left、right、parent、red
*2、关系:二叉查找树(退化单链表)—》平衡二叉树(1、复杂2、结构易破坏)—》红黑树(左中右、根叶黑、不红红、黑路同)
*3、把握要点: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,觉得更加有意义。
*单链表转红黑树的代码
*稿图(可忽略)