hashmap源码解读

hashmap使用链地址法解决哈希冲突(数组加链表),主干是一 个entry数组(每个数组包含一 个key - value键值对),主干数组 的长度
定是2的次冥,entry是hashmap的一 个静态内部类
static class Entry
<K,V> implements Map
.Entry
<K,V> {
final K key;
V value;
Entry
<K,V> next;//存储指向下一 个Entry的引用,单链表结构
int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
/
**
*
Creates new entry
.
*
/
Entry(int h, K k, V v, Entry
<K,V> n) {
value = v;
next = n;
key
= k;
hash = h;
}
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果
定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一 次寻址即可;如果
定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作
来讲,仍需遍历链表,然后通过key对象的equals方法逐一 比对查找。所以,性能考虑,HashMap中的链表出现越少,
性能才会越好。
//实际存储的key- value键值对的个数
transient int size;
//阈值,当table =={}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
threshold一 般为 capacity
*
loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到
int threshold;16
*
0.75=12 也就是达到12时就开始扩充了,但是第一 次put传入的默认为16
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结
构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;
____________________________________________________________________________________________________________________________________________________ put操作实现解读:
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为
initialCapacity 默认是1<<4(24=16)
if (table == EMPTY_TABLE) { 如果数据为空,则分配空间为16(默认)
inflateTable(threshold); //下面解释详情
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key
== null)
return putForNullKey(value);
int hash = hash(key) ;//对key的hashcode进
步计算,确保散列均匀
int i = indexFor(hash, table.length); / /获取在table中的实际位置,做与运算
for (Entry<K,V> e = table[i]; e != null; e = e.next)
Object k;
if (e.hash == hash && ((k = e.key)
== key || key
.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// HashMap 中无该映射,将该添加至该链的链头
addEntry(hash, key, value, i);
addEntry ( int hash, K key, V value, int bucketIndex)// 这是上面的 addentry 方法重写
// 上面的for不是三元运算符,而是循环,取table[i]赋值给e,当e不为空时, 判断该条链上是否存在 hash 值相同且 key 值相
等的映射,若存在,则直接覆盖 value ,并返回旧 value
这段代码意思就是先把 table[i](key 的存储位置 ) 赋值给 e, 这时候的 e 是数组元素 , 判断这个 e 是否和 key hash
相等并且判断 e key 是否和形式参数 key 相等
.
假设不相等
.
退出 if 语句 , 然后 e=e.next e 变为了数组下挂链表的第
个元素 , 再次进入 if 语句 , 这时候如果判断相等 , 并且这个链表第
个元素已经存在值了 , 那么更新这个值 (key,value),
返回旧值 , 如果 if 判断失败 , 继续向下走 , 如果 e 已经为空了 , 退出循环 ,modcount++, 修改次数加 1, 快速失败机制 ,e 为空
说明这个 (key,value) 是数组和链表中都没有的 , 那么增加到该链的表头 (addentry), 链表的表头是数组那个部分
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
----------------------------------
inflateTable方法详情解读:
private void inflateTable(int toSize) {
int capacity
= roundUpToPowerOf2(toSize); //capacity
定是2的次幂
threshold =
(int) Math.min(capacity
*
loadFactor, MAXIMUM_CAPACITY + 1); //此处为threshold赋值,取
capacity
*
loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy
定不会超过MAXIMUM_CAPACITY,除非
loadFactor大于1
table = new Entry[capacity];// 创建数组
initHashSeedAsNeeded(capacity);
}
inflateTable这个方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保
capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则
capacity
=16;to_size=16,capacity
=16;to_size=17,capacity
=32. --------------------------------------
indexfor解读
static int indexFor( int h, int length) {
return h & (length
­
1);
这个 h 实际上是对 key 进行 hash 的结果
}
-------------------------
addentry解读:
/
**
*
Adds a new entry with the specified key, value and hash code to
*
the specified bucket. It is the responsibility of this
*
method to resize the table if appropriate.
*
*
Subclass overrides this to alter the behavior of put method.
*
*
永远都是在链表的表头添加新元素
*
/
void addEntry ( int hash, K key, V value, int bucketIndex) {
// 获取 bucketIndex 处的链表
Entry<K,V> e
=
table[bucketIndex];
// 将新创建的 Entry 链入 bucketIndex 处的链表的表头
table[bucketIndex]
=
new Entry<K,V>(hash, key, value, e);
// HashMap 中元素的个数超过极限值 threshold ,则容量扩大两倍
if (size++ >
=
threshold)
resize(2
*
table. length
}
通过以上代码能够得知,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建
个长度为之
前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相
对来说是个耗资源的操作。
关于数组长度和扩容之后的数组为什么都是2的次方解读 hashmap resize
hashmap 中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查
询的效率,就要对 hashmap 的数组进行扩容,数组扩容这个操作也会出现在 ArrayList 中,所以这是
个通用的操作,
很多人对它的性能表示过怀疑,不过想想我们的
均摊
原理,就释然了,而在 hashmap 数组扩容之后,最消耗性能的点
就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize
那么 hashmap 什么时候进行扩容呢?当 hashmap 中的元素个数超过数组大小
*
loadFactor 时,就会进行数组扩
容, loadFactor 的默认值为 0.75 ,也就是说,默认情况下,数组大小为 16 ,那么当 hashmap 中元素个数超过
16
*
0.75
=
12 的时候,就把数组的大小扩展为 2
*
16
=
32 ,即扩大
倍,然后重新计算每个元素在数组中的位置,而这是
个非常消耗性能的操作,所以如果我们已经预知 hashmap 中元素的个数,那么预设元素的个数能够有效的提高
hashmap 的性能。比如说,我们有 1000 个元素 new HashMap(1000), 但是理论上来讲 new HashMap(1024) 更合
适,不过上面 annegu 已经说过,即使是 1000 hashmap 也自动会将其设置为 1024 。 但是 new HashMap(1024)
不是更合适的,因为 0.75
*
1000 < 1000, 也就是说为了让 0.75
*
size > 1000, 我们必须这样 new HashMap(2048)
才最合适,既考虑了 & 的问题,也避免了 resize 的问题。
resize 源码解读
1. void resize(int newCapacity) { // 传入新的容量
2. Entry[] oldTable
=
table; // 引用扩容前的 Entry 数组
3. int oldCapacity
= oldTable.length;
4. if (oldCapacity
== MAXIMUM_CAPACITY) { // 扩容前的数组大小如果已经达到最大 (2
^
30)
5. threshold
=
Integer.MAX_VALUE; // 修改阈值为 int 的最大值 (2
^
31
­
1) ,这样以后就不会
扩容了
6. return;
7.
}
8.
9. Entry[] newTable
=
new Entry[newCapacity]; // 初始化
个新的 Entry 数组
10. transfer(newTable); // !!将数据转移到新的 Entry 数组
/
*
将每条 Entry 重新哈希到新的数组中
*
/
11. table
=
newTable; //HashMap table 属性引用新的 Entry
数组
12. threshold
=
( int) (newCapacity
* loadFactor); // 修改阈值
13.
} transfer 源码解读
1. void transfer(Entry[] newTable) {
2. Entry[] src
=
table; //src 引用了旧的 Entry 数组
3. int newCapacity
= newTable.length;
4. for (int j
= 0; j < src.length; j++) { // 遍历旧的 Entry 数组
5. Entry<K, V> e
=
src[j]; // 取得旧 Entry 数组的每个元素
6. if (e != null) {
7. src[j]
=
null; // 释放旧 Entry 数组的对象引用( for 循环后,旧的 Entry 数组不再引用任
何对象)
8. do {
9. Entry<K, V> next
=
e.next;
10. int i = indexFor(e.hash, newCapacity); // !!重新计算每个元素在数组
中的位置 ,
*
计算在 newTable 中的位置,注意原来在同
条子链上的元素可能被分配到不同的子链
*
11. e.next
=
newTable[i]; // 标记 [1]
12. newTable[i]
=
// 将元素放在数组上
13. e
=
next; // 访问下
Entry 链上的元素
14.
} while (e != null);
15.
}
16.
}
17.
}
这里巧妙用了 do
­
while 语句 , for 语句的联合使用 , 其中 for 语句的目的是将数组中的元素转移到新的数组中或者是链
, do
­
while 目的是将每个数组下挂的链表转移到数组或者是链表中 , 特别需要注意的是,在重哈希的过程中,原属
个桶中的Entry对象可能被分到不同的桶,因为HashMap 的容量发生了变化,那么 h&(length
-
1) 的值也会发生相
应的变化。极端地说,如果重哈希后,原属于
个桶中的Entry对象仍属于同
桶,那么重哈希也就失去了意义。
__________________________________________
hashmap的线程安全问题
1、put的时候导致的多线程数据不
致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入
个key
-
value对到HashMap中,首先计算记录所要落到
的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线
程A
样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计
算出来的桶索引是
样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此
无所知,以至于它认为它应该这样做,如此
来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造
成了数据不
致的行为。
上图很重要!!!!是扩容中tranfer的具体实现图! 首先看下线程不安全的第
个原因:在代码第三行处之后,应该执行的是e=next,但是现在被t2抢占了,也就导致了e并没有向
个7移动,e还是3,代码第四行,next=e.next 在t1时候的代码第二行,e.next=null
也就是将3后面的断开了,参考下图, 所以在线程2的next为空,那么3.next=3也就是指向了自己,无限循环
现在分析第二种线程不安全的原因:
如图所示:t1执行完
次while循环
直到第二次的e=next为止都没有问题
问题出在t2抢占之后执行next=e.next (从头开始执行),这时候新表已经被t1执行过了,
next=7.next=3 t1抢占回来,执行e=next =>e=3 t2执行3.next=7 (e.next=newtable[i])
总结
下:t2抢占执行第
句 ,t1抢占执行最后
句 t2抢占执行第二句 这时候与第
句开始形成循环
要注意t1和t2拿到的是同
个内存单元对应的数据块,而不是t1拿到
个独立的,t2拿到
个独立的,这里应该参考java多线
程的内存模型
hashmap什么时候扩容?当向容器添加元素的时候,会判断当前容器的元素个数,大于阈值则扩容
—————————————————————————————————— hashmap在jdk1.8下的实现:
种自平衡二叉查找树,可以在 O(log n) 时间内做查找,插入和删除, jdk8 之后 HashMap 桶内链表长度超过树化阀值
且总长度超过最小树化容量后会将链表转换为红黑树。
几个需要了解的知识点(JDK1.8)
头节点指的是table表上索引位置的节点,也就是链表的头节点。
根结点(root节点)指的是红黑树最上面的那个节点,也就是没有父节点的节点。
红黑树的根结点
定是索引位置的头结点(也就是链表的头结点),通过moveRootToFront方法来维持。
转为红黑树节点后,链表的结构还存在,通过next属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转
为红黑树节点,链表结构就不存在了。 在红黑树上,叶子节点也可能有next节点,因为红黑树的结构跟链表的结构是互不影响的,不会因为是叶子节点就说该
节点已经没有next节点。
源码中
些变量定义:如果定义了
个节点p,则pl为p的左节点,pr为p的右节点,pp为p的父节点,ph为p的hash值,
pk为p的key值,kc为key的类等等。源码中很喜欢在if/for等语句中进行赋值并判断,请注意。
链表中移除
个节点只需如下图操作,其他操作同理。
源码中进行红黑树的查找时,会反复用到以下两条规则:1)如果目标节点的hash值小于p节点的hash值,则向p节点的
左边遍历;否则向p节点的右边遍历。2)如果目标节点的key值小于p节点的key值,则向p节点的左边遍历;否则向p节
点的右边遍历。这两条规则是利用了红黑树的特性(左节点<根结点<右节点)。
源码中进行红黑树的查找时,会用dir(direction)来表示向左还是向右查找,dir存储的值是目标节点的hash/key与p节
点的hash/key的比较结果。
______________________________________________________
源码解读:
定位哈希桶数组索引位置
方法解读:
对于任意给定的对象,只要它的hashCode()返回值相同,那么计算得到的hash值总是相同的。我们首先想到的就是把
hash值对table长度取模运算,这样
来,元素的分布相对来说是比较均匀的。
但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此JDK团队对取模运算进行了优化,使用上面
代码2的位与运算来代替模运算。这个方法非常巧妙,它通过
(table.length
-
1) & h
来得到该对象的索引位置,这个
优化是基于以下公式:x mod 2
^
n = x & (2
^
n
-
1)。我们知道HashMap底层数组的长度总是2的n次方,并且取模运算
h mod table.length
,对应上面的公式,可以得到该运算等同于
h & (table.length
-
1)
。这是HashMap在速
度上的优化,因为&比%具有更高的效率。
在JDK1.8的实现中,还优化了高位运算的算法,将hashCode的高16位与hashCode进行异或运算,主要是为了在table
的length较小的时候,让高位也参与运算,并且不会有太大的开销。
下图是
个简单的例子,table长度为16: _______________________________
GET方法:
注释加的很详细,先对table(数组+链表+红黑树中的数组部分进行校验)进行校验,校验是否为空,length是否大于0
使用table.length
-
1和hash(上面key的hash)值进行位与运算,得出在table上的索引位置,将该索引位置的节点赋值给
first节点
first节点的由来:
校验该索引位置是否为空
检查first节点的hash值和key是否和入参的
样,如果
样则first即为目标节点,直接返回first节点
如果first的next节点不为空则继续遍历
如果first节点为TreeNode,则调用getTreeNode方法(见下文代码块1)查找目标节点
如果first节点不为TreeNode,则调用普通的遍历链表方法查找目标节点
如果查找不到目标节点则返回空 代码块2:find方法: 校验table是否为空或者length等于0,如果是则调用resize方法(见下文resize方法)进行初始化
通过hash值计算索引位置,将该索引位置的头节点赋值给p节点,如果该索引位置节点为空则使用传入的参数新增
个节
点并放在该索引位置
判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
如果p节点不是目标节点,则判断p节点是否为TreeNode,如果是则调用红黑树的putTreeVal方法(见下文代码块4)查
找目标节点
走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,并定义变量binCount来统计该链表的节点数
如果p的next节点为空时,则代表找不到目标节点,则新增
个节点并插入链表尾部,并校验节点数是否超过8个,如果
超过则调用treeifyBin方法(见下文代码块6)将链表节点转为红黑树节点
如果遍历的e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
如果插入节点后节点数超过阈值,则调用resize方法(见下文resize方法)进行扩容
------------------------------------------
putTreeVal方法
kc = comparableClassFor(k))
== null
表示该类本身不可比(class C don
'
t implements
Comparable<C>);
dir = compareComparables(kc, k, pk))
== 0
表示k与pk对应的Class之间不可比。
searched为
次性开关仅在p为root时生效,遍历比较左右子树中是否存在于插入节点相等的。1 从根节点遍历槽点的红
黑树; .2 判断待插入节点位于左子树还是右子树, key相等时直接返回, 由主流程判断是否更新节点值;
.3 当遍历到当前节点的左(右)子节点为空时, 插入待插入节点;
.4 再次平衡红黑树;
判断节点位于左右子树的过程在前面的HashMap::get中已经详细的讲解过了, 先比较hash再比较key是否实现了
Comparable接口;如果不实现时, 调用HashMap
.TreeNode::tieBreakOrder, 我们来看下tieBreakOrder方法做了什么:
tie bread在网球比赛中叫做平局决胜, 如果把判断节点位于那颗子树作为比赛的话:
.1 比较节点的hash值是第
轮;
.2 通过Comparable比较是第二轮;
.3 如果前面两轮没有分出结果, 那么tieBreakOrder就作为决胜轮来比较出
个结果;
.4 当然如果key没有实现Comparable接口, 那么第
轮没结果就会直接进入决胜轮;
https://blog
.csdn.net/weixin_42340670/article/details/80635008 源码链接
大致流程:先是比较根节点的hash,如果hash没有比较出来,再比较key是否实现了comparable接口,如果仍然没有实现,那
么调用tieBreakorder,这个方法是
定能比较出来的,如果经过上述比较,dir<0,或者是>0,看下面代码
if ((p
=
(dir <= 0) ? p
.left : p
.right)
== null) {
// 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
Node<K,V> xpn = xp
.next; // 获取当前节点的next节点
TreeNode<K,V> x = map
.newTreeNode(h, k, v, xpn); // 创建
个新的树节点
if (dir <= 0)
xp
.left = x; // 左孩子指向到这个新的树节点
else
xp
.right = x; // 右孩子指向到这个新的树节点
xp
.next = x; // 链表中的next节点指向到这个新的树节点
x.
parent = x.
prev = xp; // 这个新的树节点的父节点、前节点均设置为 当前的树节点
if (xpn != null) // 如果原来的next节点不为空
((TreeNode<K,V>)xpn)
.
prev = x; // 那么原来的next节点的前节点指向到新的树节点
moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根节点置顶
return null; // 返回空,意味着产生了
个新节点
次循环的时候这个p为root,如果dir小于0,并且p的左结点不为空,那么if语句判断失败,直接进入下
次循环,但是
p
=
p
.left已经生效,所以第二次的p是root的左结点,以此类推,如果第二次的dir大于0,p的右结点为空,那么执行if里面的插入
操作,就是修改
系列的指针,这个没有难度
. __________________________________
treeifybin方法:
这个是比较好理解的,将链表的单向链表改为了红黑树形式的双向,结点改为树形结点
.
并把所有的树形结点以头节点为根节点,构建红黑树
如下是上述代码过程详解:
校验table是否为空,如果长度小于64,则调用resize方法(见下文resize方法)进行扩容。
根据hash值计算索引值,将该索引位置的节点赋值给e节点,从e节点开始遍历该索引位置的链表。
调用replacementTreeNode方法(该方法就
行代码,直接返回
个新建的TreeNode)将链表节点转为红黑树节点,将头结点赋值给
hd节点,每次遍历结束将p节点赋值给tl,用于在下
次循环中作为上
个节点进行
些链表的关联操作(p
.
prev = tl 和 tl.next =
p)。
将table该索引位置赋值为新转的TreeNode的头节点hd,如果该节点不为空,则以hd为根结点,调用treeify方法(见下文代码块7)构建
红黑树。
while(e=e.next)!=null是推动向下的关键
/
**
*
tab:元素数组,
*
hash:hash值(要增加的键值对的key的hash值)
*
/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e; /
*
*
如果元素数组为空 或者 数组长度小于 树结构化的最小限制
*
MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换
*
个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相
同)
*
因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组
位置上。
*
/
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // 扩容,可参见resize方法解析
// 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
// 根据hash值和数组长度进行取模运算后,得到链表的首节点
else if ((e = tab[index =
(n
-
1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null; // 定义首、尾节点
do {
TreeNode<K,V> p
= replacementTreeNode(e, null); // 将该节点转换为 树节点
if (tl == null) // 如果尾节点为空,说明还没有根节点
hd =
p; // 首节点(根节点)指向 当前节点
else { // 尾节点不为空,以下两行是
个双向链表结构
p
.
prev = tl; // 当前树节点的 前
个节点指向 尾节点
tl.next =
p; // 尾节点的 后
个节点指向 当前节点
}
tl =
p; // 把当前节点设为尾节点
} while ((e = e.next) != null); // 继续遍历链表
// 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表
// 把转换后的双向链表,替换原来位置上的单向链表
if ((tab[index]
= hd) != null)
hd.treeify(tab);//此处单独解析
}
}
_____________________________________________
构建红黑树:treeify方法:
已看懂:
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null; // 定义树的根节点
for (TreeNode<K,V> x = this, next; x != null; x = next) { // 遍历链表,x指向当前节点、next指向下
个节点
next =
(TreeNode<K,V>)x.next; // 下
个节点
x.left = x.right = null; // 设置当前节点的左右节点为空
if (root == null) { // 如果还没有根节点
x.
parent = null; // 当前节点的父节点设为空
x.red = false; // 当前节点的红色属性设为false(把当前节点设为黑色)
root = x; // 根节点指向到当前节点
}
else { // 如果已经存在根节点了
K k = x.key; // 取得当前链表节点的key
int h = x.hash; // 取得当前链表节点的hash值
Class<?> kc = null; // 定义key所属的Class
for (TreeNode<K,V> p
= root;;) { // 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出 // GOTO1
int dir, ph; // dir 标识方向(左右)、ph标识当前树节点的hash值
K pk =
p
.key; // 当前树节点的key
if ((ph =
p
.hash) > h) // 如果当前树节点hash值 大于 当前链表节点的hash值
dir =
-
1; // 标识当前链表节点会放到当前树节点的左侧
else if (ph < h)
dir = 1; // 右侧
/
*
*
如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
*
如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的
方式再比较两者。
*
如果还是相等,最后再通过tieBreakOrder比较
*
/
else if ((kc == null &&
(kc = comparableClassFor(k))
== null) ||
(dir = compareComparables(kc, k, pk))
== 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp
=
p; // 保存当前树节点
/
*
*
如果dir 小于等于0 : 当前链表节点
定放置在当前树节点的左侧,但不
定是该树节点的左孩子,也可能是左孩子的右孩子
或者 更深层次的节点。
*
如果dir 大于0 : 当前链表节点
定放置在当前树节点的右侧,但不
定是该树节点的右孩子,也可能是右孩子的左孩子 或
者 更深层次的节点。
*
如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点 再从GOTO1 处开始 重新寻找自
己(当前链表节点)的位置
*
如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
*
挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下
个链表节点进行处理了。
*
/
if ((p
=
(dir <= 0) ? p
.left : p
.right)
== null) {
x.
parent = xp; // 当前链表节点 作为 当前树节点的子节点
if (dir <= 0)
xp
.left = x; // 作为左孩子
else
xp
.right = x; // 作为右孩子
root = balanceInsertion(root, x); // 重新平衡
break;
}
}
}
}
// 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪
个节点是不确定的
// 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象
定根节点对象,而目前只是链表的第
个节点对象,所以要做相应
的处理。
moveRootToFront(tab, root); // 单独解析
下面进行具体分析: for (TreeNode<K,V> x = this, next; x != null; x = next) 首先是从链表开始遍历,但是现在这里的链表已经是双向链
表了
.x为链表的第
个元素
.
定是第
个,因为现在没有进行任何操作,treebinfiy if ((tab[index]
= hd) != null) 将头节点作为参数传入
hd.treeify(tab);//
假设现在有 1
-
4
-
3
-
2
-
5 的链表要变成红黑树 现在x是第
个,但是现在并没有根节点,那么符合第
个if的条件,将X作为红黑树的根结点
.
次for循环就结束了,后面都是else的结果,进行第二次循环,第二次循环这个x为4,已经存在根节点了,根节点为1,进入到else的for循环,p为
根节点,也就是1,通过
系列的比较,假如1的hash要小于4的hash,dir为1,进入到 if ((p
=
(dir <= 0 ) ? p
.left : p
.right)
== null ) {,通过判
断,p
=
p
.right,
==null符合条件,
p(root)的右子树确实没有定义,
x.
parent = xp; // 当前链表节点 作为 当前树节点的子节点
if (dir <= 0)
xp
.left = x; // 作为左孩子
else
xp
.right = x; // 作为右孩子
root = balanceInsertion(root, x); // 重新平衡
break;
解读下这段:现在xp也就是p是root,x是4,dir=1,所以将root赋予4.
父节点,
并且x是root的右孩子,调整平衡(平衡之后根节点可能就变化了,这个不影响本段代码),跳出第二个for循环,继续进入到第
个for循环,这时候
x为3,进入到else的for循环中,p仍然中root开始遍历,假设3的hash大于root的hash,dir=1,并且p
=
p
.right不为null 为4,所以不满足if语句,p
为p
.rightj进行下
次for,p为4
4的hash大于3的hash,所以dir=
-
1,所以p
=
p
.left x是xp的左孩子,调整平衡,以此类推!
_____________________________
resize
/当前所有元素所在的数组,称为老的元素数组
int oldCap
=
(oldTab == null) ? 0 : oldTab.length; //老的元素数组长度
int oldThr = threshold; // 老的扩容阀值设置
int newCap, newThr = 0; // 新数组的容量,新数组的扩容阀值都初始化为0
if (oldCap > 0) { // 如果老数组长度大于0,说明已经存在元素
// PS1
if (oldCap >= MAXIMUM_CAPACITY) { // 如果数组元素个数大于等于限定的最大容量(2的30次方)
// 扩容阀值设置为int最大值(2的31次方
-
1 ),因为oldCap再乘2就溢出了。
threshold = Integer.MAX_VALUE;
return oldTab; // 返回老的元素数组
}
/
*
*
如果数组元素个数在正常范围内,那么新的数组容量为老的数组容量的2倍(左移1位相当于乘以2)
*
如果扩容之后的新容量小于最大容量 并且 老的数组容量大于等于默认初始化容量(16),那么新数组的扩容阀值设置为老阀
值的2倍。(老的数组容量大于16意味着:要么构造函数指定了
个大于16的初始化容量值,要么已经经历过了至少
次扩容)
*
/
else if ((newCap
= oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// PS2
// 运行到这个else if 说明老数组没有任何元素
// 如果老数组的扩容阀值大于0,那么设置新数组的容量为该阀值
// 这
步也就意味着构造该map的时候,指定了初始化容量。
else if (oldThr > 0) // initial capacity was placed in threshold
newCap
= oldThr;
else { // zero initial threshold signifies using defaults
// 能运行到这里的话,说明是调用无参构造函数创建的该map,并且第
次添加元素
newCap
= DEFAULT_INITIAL_CAPACITY; // 设置新数组容量 为 16 newThr =
(int)(DEFAULT_LOAD_FACTOR
*
DEFAULT_INITIAL_CAPACITY); // 设置新数组扩容阀值为 16
*
0.75 = 12。0.75为
负载因子(当元素个数达到容量了4分之3,那么扩容)
}
// 如果扩容阀值为0 (PS2的情况)
if (newThr == 0) {
float ft =
(float)newCap
*
loadFactor;
newThr =
(newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); // 参见:PS2
}
threshold = newThr; // 设置map的扩容阀值为 新的阀值
@SuppressWarnings({
"
rawtypes
"
,
"
unchecked
"
})
// 创建新的数组(对于第
次添加元素,那么这个数组就是第
个数组;对于存在oldTab的时候,那么这个数组就是要需要扩容
到的新数组)
Node<K,V>[] newTab =
(Node<K,V>[])new Node[newCap];
table = newTab; // 将该map的table属性指向到该新数组
if (oldTab != null) { // 如果老数组不为空,说明是扩容操作,那么涉及到元素的转移操作
for (int j
= 0; j
< oldCap; ++
j) { // 遍历老数组
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 如果当前位置元素不为空,那么需要转移该元素到新数组
oldTab[j]
= null; // 释放掉老数组对于要转移走的元素的引用(主要为了使得数组可被回收)
if (e.next == null) // 如果元素没有有下
个节点,说明该元素不存在hash冲突
// PS3
// 把元素存储到新的数组中,存储到数组的哪个位置需要根据hash值和数组长度来进行取模
// 【hash值 % 数组长度】
=
【 hash值 & (数组长度
-
1)】
// 这种与运算求模的方式要求 数组长度必须是2的N次方,但是可以通过构造函数随意指定初始化容量呀,如果指定了
17,15这种,岂不是出问题了就?没关系,最终会通过tableSizeFor方法将用户指定的转化为大于其并且最相近的2的N次方。 15
-
> 16、
17
-
> 32
newTab[e.hash & (newCap
-
1)]
= e;
// 如果该元素有下
个节点,那么说明该位置上存在
个链表了(hash相同的多个元素以链表的方式存储到了老数组的这
个位置上了)
// 例如:数组长度为16,那么hash值为1(1%16=1)的和hash值为17(17%16=1)的两个元素都是会存储在数组的第
2个位置上(对应数组下标为1),当数组扩容为32(1%32=1)时,hash值为1的还应该存储在新数组的第二个位置上,但是hash值为
17(17%32=17)的就应该存储在新数组的第18个位置上了。
// 所以,数组扩容后,所有元素都需要重新计算在新数组中的位置。
else if (e instanceof TreeNode) // 如果该节点为TreeNode类型
((TreeNode<K,V>)e)
.split(this, newTab, j, oldCap); // 此处单独展开讨论
else { // preserve order
Node<K,V> loHead = null, loTail = null; // 按命名来翻译的话,应该叫低位首尾节点
Node<K,V> hiHead = null, hiTail = null; // 按命名来翻译的话,应该叫高位首尾节点
// 以上的低位指的是新数组的 0 到 oldCap
-
1 、高位指定的是oldCap 到 newCap
-
1
Node<K,V> next;
// 遍历链表
do {
next = e.next;
// 这
步判断好狠,拿元素的hash值 和 老数组的长度 做与运算
// PS3里曾说到,数组的长度
定是2的N次方(例如16),如果hash值和该长度做与运算,结果为0,就说明该hash
值小于数组长度(例如hash值为7), // 那么该hash值再和新数组的长度取摸的话mod值也不会发生变化,所以该元素的在新数组的位置和在老数组的位置
是相同的,所以该元素可以放置在低位链表中。
if ((e.hash & oldCap)
== 0) {
// PS4
if (loTail == null) // 如果没有尾,说明链表为空
loHead = e; // 链表为空时,头节点指向该元素
else
loTail.next = e; // 如果有尾,那么链表不为空,把该元素挂到链表的最后。
loTail = e; // 把尾节点设置为当前元素
}
// 如果与运算结果不为0,说明hash值大于老数组长度(例如hash值为17)
// 此时该元素应该放置到新数组的高位位置上
// 例:老数组长度16,那么新数组长度为32,hash为17的应该放置在数组的第17个位置上,也就是下标为16,那么下
标为16已经属于高位了,低位是[0
-
15],高位是[16
-
31]
else { // 以下逻辑同PS4
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) { // 低位的元素组成的链表还是放置在原来的位置
loTail.next = null;
newTab[j]
= loHead;
}
if (hiTail != null) { // 高位的元素组成的链表放置的位置只是在原有位置上偏移了老数组的长度个位置。
hiTail.next = null;
newTab[j
+ oldCap]
= hiHead; // 例:hash为 17 在老数组放置在0下标,在新数组放置在16下标; hash为 18 在
老数组放置在1下标,在新数组放置在17下标;
————————
resize的关键部分! 数据的转移!
if (oldTab != null) { // 如果老表不为空, 则需遍历将节点赋值给新表
for (int j
= 0; j
< oldCap; ++
j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 将索引值为j的老表头节点赋值给e
oldTab[j]
= null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
// 如果e.next为空, 则代表老表的该位置只有1个节点,
// 通过hash值计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
newTab[e.hash & (newCap
-
1)]
= e;
else if (e instanceof TreeNode)
// 调用treeNode的hash分布(跟下面最后
个else的内容几乎相同)
((TreeNode<K,V>)e)
.split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null; // 存储跟原索引位置相同的节点
这里的lohead是指新数组低位的链表的表头,lotail是指新数组低位的链表的表尾
Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:原索引+oldCap的节点
这里的hihead指的是新数组高位的链表的表头,也就是原索引+oldcap位置链表的表头 hitail指的是新数组高位链表的表尾
Node<K,V> next;
do {
next = e.next;
//如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置
if ((e.hash & oldCap)
== 0) {
if (loTail == null) // 如果loTail为空, 代表该节点为第
个节点
loHead = e; // 则将loHead赋值为第
个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
}
//如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail为空, 代表该节点为第
个节点
hiHead = e; // 则将hiHead赋值为第
个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null; // 最后
个节点的next设为空
newTab[j]
= loHead; // 将原索引位置的节点设置为对应的头结点
}
if (hiTail != null) {
hiTail.next = null; // 最后
个节点的next设为空
newTab[j
+ oldCap]
= hiHead; // 将索引位置为原索引+oldCap的节点设置为对应的头结点
______________________________________________
解读上述代码:从j
=0开始遍历,也就是从就数组的第
个元素开始,如果什么也没挂,直接放在新数组的,如果下挂的是红黑树,拆分进行转移,重点:如
果下挂的是链表,进入到do
-
while循环中,将这个链表所有数据开始转移。如果e(数组元素)的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟
老表的索引位置
样,如果loTail为空,说明没有尾结点,说明这个链表就e
个元素,把e当做头结点,否则这个放在链表的最后面(与jdk1.7不
样,这里
采用尾插法),以此类推,假设第
串链表放完,进入第二次for循环,开始第二串链表,假如e.hash & oldCap) != 0 那么第二串放在j + oldCap的位置,
也就是进入到高位,执行else语句,假如只有两串的话,for循环完毕,进入到最后的两个if语句, newTab[j]
= loHead; newTab[j + oldCap]
= hiHead
这两句话很重要, loHead = e hiHead = e 配合这两句话,也就是假如e在低位,那么e在newtab【j】的位置,假如e在高位,e在newtab{j+oldcap}的位
置。也就是把两个首元素(高位和低位)放在桶里,所以顺序是先拼完了链表,跳出for循环,再转移到桶里去(两个if) 复制过程, a 过去,假设计算后位置不边,进到 i, 此时 i null a 进去后即是 head ,又是 tail
然后循环,到 b ,假设计算后还是 i i 中已经有 a ,所以 b 直接丢到 a 后面, a 任是 head, tail 已经变成了 b
以此类推, a,b,c,d 都会放在 i,j
其实是先拼完链表才装进桶里的,这里只是方便描述,说成是
个过去
————————————————————————————
扩容时rehash原理():
remove方法: 已理解
我们先来看remove方法
/
**
*
从HashMap中删除掉指定key对应的键值对,并返回被删除的键值对的值
*
如果返回空,说明key可能不存在,也可能key对应的值就是null
*
如果想确定到底key是否存在可以使用containsKey方法
*
/
public V remove(Object key) {
Node<K,V> e; // 定义
个节点变量,用来存储要被删除的节点(键值对)
return (e = removeNode(hash(key), key, null, false, true))
== null ?
null : e.value; // 调用removeNode方法
}
可以发现remove方法底层实际上是调用了removeNode方法来删除键值对节点,并且
根据返回的节点对象取得key对应的值,那么我们再来详细分析下removeNode方法的
代码
/
**
*
方法为final,不可被覆写,子类可以通过实现afterNodeRemoval方法来增加自己的
处理逻辑(解析中有描述)
* *
@param hash key的hash值,该值是通过hash(key)获取到的
*
@param key 要删除的键值对的key
*
@param value 要删除的键值对的value,该值是否作为删除的条件取决于
matchValue是否为true
*
@param matchValue 如果为true,则当key对应的键值对的值equals(value)为true
时才删除;否则不关心value的值
*
@param movable 删除后是否移动节点,如果为false,则不移动
*
@return 返回被删除的节点对象,如果没有删除任何节点则返回null
*
/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index; // 声明节点数组、当前节点、数组
长度、索引值
/
*
*
如果 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节
点为 树的根节点 或 链表的首节点)不为空
*
需要从该节点p向下遍历,找到那个和key匹配的节点对象
*
/
if ((tab = table) != null && (n = tab.length) > 0 &&
(p
= tab[index =
(n
-
1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v; // 定义要返回的节点对象,声明
个临时
节点变量、键变量、值变量
// 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node,
首节点
if (p
.hash == hash &&
((k =
p
.key)
== key || (key != null && key
.equals(k))))
node =
p;
/
*
*
到这
步说明首节点没有匹配上,那么检查下是否有next节点
*
如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就
个节点并
且还没匹配上,也就没得删了,最终也就返回null了 *
如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在
链表,也可能是
颗红黑树
*
/
else if ((e =
p
.next) != null) {
// 如果当前节点是TreeNode类型,说明已经是
个红黑树,那么调用
getTreeNode方法从树结构中查找满足条件的节点
if (p instanceof TreeNode)
node =
((TreeNode<K,V>)p)
.
getTreeNode(hash, key);
// 如果不是树节点,那么就是
个链表,只需要从头到尾逐个节点比对即可
else {
do {
// 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node
变量,调出循环
if (e.hash == hash &&
((k = e.key)
== key ||
(key != null && key
.equals(k)))) {
node = e;
break;
}
// 走到这里,说明e也没有匹配上
p
= e; // 把当前节点p指向e,这
步是让p存储的永远下
次循环里e的
父节点,如果下
次e匹配上了,那么p就是node的父节点
} while ((e = e.next) != null); // 如果e存在下
个节点,那么继续去匹配下
个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
}
}
/
*
*
如果node不为空,说明根据key匹配到了要删除的节点
*
如果不需要对比value值 或者 需要对比value值但是value值也相等
*
那么就可以删除该node节点了
*
/ if (node != null && (!matchValue || (v = node.value)
== value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) // 如果该节点是个TreeNode对象,说明此节
点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点
((TreeNode<K,V>)node)
.removeTreeNode(this, tab, movable);
else if (node ==
p) // 如果该节点不是TreeNode对象,node ==
p 的意思是
该node节点就是首节点, 最上面的node=
p
tab[index]
= node.next; // 由于删除的是首节点,那么直接将节点数组对应
位置指向到第二个节点即可
else // 如果node节点不是首节点,此时p是node的父节点,由于要删除
node,所有只需要把p的下
个节点指向到node的下
个节点即可把node从链表中删
除了
p
.next = node.next;
++modCount; // HashMap的修改次数递增
--
size; // HashMap的元素个数递减
afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法
HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写
return node;
}
}
return null;
________________________________
removetreenode方法:先略过
------------------------------------------------------
解释2:关于红黑树的平衡调整?
答:红黑树的操作涉及的操作比较复杂,三言两语无法说清。有兴趣的可以去单独学
习,本文由于篇幅关系暂不详细介绍红黑树的具体操作,在这简单的介绍:红黑树是
种自平衡二叉树,拥有优秀的查询和插入/删除性能,广泛应用于关联数组。
对比AVL树,AVL要求每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1,
而红黑树通过适当的放低该条件(红黑树限制从根到叶子的最长的可能路径不多于最短
的可能路径的两倍长,结果是这个树大致上是平衡的),以此来减少插入/删除时的平衡
调整耗时,从而获取更好的性能,而这虽然会导致红黑树的查询会比AVL稍慢,但相比
插入/删除时获取的时间,这个付出在大多数情况下显然是值得的。 在HashMap中的应用:HashMap在进行插入和删除时有可能会触发红黑树的插入平衡
调整(balanceInsertion方法)或删除平衡调整(balanceDeletion )方法,调整的方
式主要有以下手段:左旋转(rotateLeft方法)、右旋转(rotateRight方法)、改变节
点颜色(x.red = false、x.red = true),进行调整的原因是为了维持红黑树的数据结
构。
_____
-
hashmap总结:
1.hashmap实现了cloneable接口,实现了clone方法,就是克隆
个hashmap对象并返回
HashMap 实现 java.io.Serializable ,分别实现了串行读取、写入功能。
串行写入函数是 writeObject() ,它的作用是 HashMap
总的容量,实际容量,所有的 Entry
都写入到输出流
中。
而串行读取函数是 readObject() ,它的作用是 HashMap
总的容量,实际容量,所有的 Entry
依次读出
2.hashmap遍历方式:
2.1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值