HashMap(jdk1.8)源码解读导航

前言:这里只是HashMap代码理解,就是看了这些能方便你看懂源码,具体的详细源码注释网上都可以查到。

构造方法

构造方法一共4个,分别是
1、HashMap() // 无参构造
2、HashMap(int initialCapacity) // 设置初始容量
3、HashMap(int initialCapacity, float loadFactor) // 设置初始容量和负载系数
4、HashMap(Map<? extends K, ? extends V> m) // 将Map m的key,value插入到新的HashMap中

这里重点说第3种构造方法
负载系数loadFactor(默认值0.75f),判断为大于0 的浮点数
初始容量initialCapacity(默认值16),初始容量要大于0,当设置容量超过hashMap最大容量MAXIMUM_CAPACITY(2的30次方)时,容量会设置成最大值。
然后执行 tableSizeFor(initialCapacity) 方法。
操作:将初始容量设置成大于初始等于容量的最小2次幂。换句话说,就是比初始容量大(或等于)的最小的2的n次方的值。比如,传入的为9,就会改为16,传入的是63,就会改为64。
目的:插入元素计算桶位时,可以使每个桶位都有命中的机会。

插入方法put(K key, V value)

首先执行hash(key)
操作:将key的hashCode值与该值右移16位的结果进行异或运算。
目的:让hashCode的高16位参与运算,降低hash冲突,提高hashMap的散列性。
因为,计算出来的hash值,要与数组下标减一进行按位与操作(这也就是数组下标(初始容量)必须为2的n次幂的原因),计算出对应的桶位。如果数组很小的话,按位与操作就会直接把高位省略。

putVal方法
首先判断数组是否为空,如果为空执行 resize() 方法,进行数组的创建。所以说,HashMap在构造方法的时候并没有创建数组,是在第一次put的时候才创建数组的。
然后计算桶位,判断桶位是否为空,为空,添加一个新的Node对象
如果不为空则存在三种情况。
1、如果该桶的第一个值恰好和插入的key一样,则替换value
2、如果key不一样,如果该桶有已经树化,则进行红黑树的添加。执行putTreeVal方法
3、如果没有树化则说明是链表,遍历链表,如果有相同的key则替换,如果没有相同的key,则尾插法插入新值。之后进行判断,如果链表长度大于等于8,执行treeifyBin方法。
根据三种的返回情况,判断如果返回存在,则说明是替换值,返回老的value,如果不存在,则说明是添加值。然后判断加一之后的size和扩容阈值threshold(resize() 方法中获得,是初始容量和负载系数的乘积)比较,如果大于threshold,则执行 resize() 方法扩容,所以 resize() 方法既有初始化操作,也有扩容操作。

putTreeVal方法
hash值比较,小的设置成-1从左边找,大的设置成1从右边找,找hash值一样的,再判断key是否一样,如果key一样,返回就好。如果不一样,还要判断是否继承Comparable接口,然后再通过compareTo计算大小,如果还一样,通过find方法左右都找找,看有没有一样的,如果全一样,只有通过tieBreakOrder方法强行分出谁大谁小。
如果发现左边或者右边为空,说明要添加一个新元素,然后创建新元素添加到红黑树中,这里不光在红黑树中添加,同时在链表中添加。没错,红黑树内部同样维护着链表。然后进行红黑树平衡操作balanceInsertion方法,然后是链表维护moveRootToFront方法,将根节点放到内部维护链表的头部。因为要把根节点放入数组的桶中,根节点放在链表头方便遍历链表。

balanceInsertion方法
红黑树特性:根节点为黑色,叶子节点都是黑色的空节点,叶子节点到达根节点经过的黑色节点数是相同的,俗称“黑高”,两个红色节点不能相连(红色节点的父节点和子节点(如果存在)一定为黑色),红黑树新插入的节点为红色,插入之后再进行平衡操作,保证以上特性。
平衡操作:
1、父为空,说明是根节点,直接染黑然后返回。
2、父为黑色,插入不破坏红黑树性质,返回根节点。
3、父节点红色
3.1 叔叔存在且为红,父叔变黑,爷变红,然后从爷爷结点开始处理。(就是把爷爷节点变成当前节点)
3.2 叔叔不存在或为黑,
LL红,父黑爷红,爷爷为节点右旋。
LR红,父亲为节点左旋,变成LL红,然后进行LL红操作。
RR红,父黑爷红,爷爷为节点左旋。
RL红,父亲为节点右旋,变成RR红,然后进行RR红操作。

树化操作treeifyBin方法
首先会判断,数组长度是否小于64,如果小于,调用 resize() 进行扩容。
数组长度大于等于64,则循环链表把node变成treeNode,连接顺序不变。然后循环新的treeNode链表,调用treeify方法进行一个个的添加操作和putTreeVal方法类似,就是少了查的部分。

resize() 方法
目的:获取扩容后的Node数组,把老数组的数据放入新数组中。
操作:具体步骤省略,这里提两个不容易理解的点。
1、老数组长度 oldCap 左移1位小于最大数组,且oldCap本身要大于默认值16,才可以给老的扩容阈值 oldThr 左移1位。我个人认为,是因为数组长度太小,计算的出来的新的扩容阈值newCap和oldThr << 1之间的误差太大。
2、老数组不存在时,老的扩容阈值 oldThr > 0,此时说明是构造方法中传入了初始容量initialCapacity,此时oldThr 中存放的是计算好的2的n次幂的数组长度。所以直接赋值就好,新的扩容阈值 newThr会在下面计算。

获得了新数组长度和新的扩容阈值,之后创建新数组,再将老数组的值搬家就好了。这里分几种情况。
1、老数组桶位为空,跳过
2、老数组桶位为一个node,用新数组的数组长度减一和该节点的hash值进行与操作,得到的桶位直接赋值就好。
3、新数组桶位是一个红黑树,执行split方法因为之前提到,红黑树内部维护了一个链表,将链表拆分成高位链和低位链,分别判断长度,小于等于6则转换成链表。执行untreeify方法本质就是把treeNode强转成Node,顺序不变。大于6则用treeify方法转化成树。这里低位链在原位不动,高位链放在原位加老数组长度的位置。
4、链表的话,同样是分高位链和低位链。低位链不动,高位链放在当前位置加原数组长度的位置上。

区分高低位链的原因:这里我简单说一下,网上有更详细的解答(最好看视频)。就是数组扩大了一倍,原来求数组下标的hash值的有效位就增加了一位,所以之前的hash值的有效位是1,也可能有效位为0。为0的话就在低位链,为1 的话就在高位链。这么区分就是为了提高扩容效率,其实一个个的进行hash值与数组下标减一的与操作是可以的。jdk1.7就是这么整的。

删除方法remove(Object key)

删除方法,分节点删除,链表删除,红黑树删除。前两种比较简单。重点说红黑树删除。
源码中是先查到,然后再删除,毕竟查不到也没删的意义。
如果删除节点是TreeNode,调用removeTreeNode方法
首先是将内部维护的链表删除,然后判断当前红黑树性质,如果右为空或者左为空,或者左左为空,则将2红黑树转化成链表,所以红黑树删除不是到达6个节点就转换成链表的。

PS:最少的情况是3个节点,根节点的孙子节点都没有,并且上次删除的是左左,然后删1个是2个。最多的情况是右边枝繁叶茂,根据红黑树特性可以算到共9个节点,并且上一次删除的是左左,然后删除1个是8个节点。

然后开始最重要的删除操作:
首先要明确一个点,删除红色子节点是不影响二叉树平衡的。
1、删除节点存在右孩子,然后找到它右孩子最最最最左边的那个节点。找到之后替换颜色和位置。替换之后删除节点还存在右孩子,此时他的左为空,右孩子一定是红色。然后直接跟右孩子换颜色和位置,最后直接删除就好。
2、删除节点不存在有孩子,如果存在左孩子,则左孩子一定是红色,跟左孩子换颜色和位置,直接删除就好。
3、删除节点本身左右孩子都没有,或者替换之后的左右孩子都没有。说明此时删除节点一定在叶子节点上(不考虑空叶子)。此时判断删除节点是红还是黑,如果是红,直接删除,如果是黑则要调用balanceDeletion方法平衡红黑树。

balanceDeletion方法
调用此方法时,删除的节点一定是黑色的叶子节点。并且一定有兄弟节点。(根节点除外)
1、如果当前是根节点,染黑,返回。
2、如果当前节点是红节点,染黑,返回root
3、删除节点为左黑节点。
3.1: 若右兄弟为红,则父变红,兄变黑,以父亲为节点左旋。如果之前的右兄弟有左节点,xpr 指向该节点。否则当前节点指向父节点,继续循环。(这里应该不会为空,因为右兄弟是红色节点,为了实现黑高,一定会有两个黑色子节点)
3.2: 若右兄弟为黑,则 xpr 指向右兄弟
3.3: 此时xpr指向都是黑色节点。接下来判断。如果xpr两个子节点都是黑色节点或者都为空,xpr染红,当前节点指向父节点,继续循环。
PS:第一次循环的时候不会出现两个黑色节点,之后的循环有可能出现。
3.4: 如果右空或者右黑,左节点存在染黑,xpr染红,然后以xpr为节点右旋,xpr指向xp.right,也就是旋转之后的左节点。
PS:左节点一定存在,因为我如果左节点不存在就不会存在右黑的情况,右空的情况,在上面的判断中过滤了
3.5: 然后xpr与xp颜色保持一致,xpr右孩子染黑。
3.6: xp染黑,以xp为节点左旋。
4、删除节点为右黑节点,与左节点方法一致,就是方向反过来。

总结:以删除左节点为例:
兄红左旋左节点,兄黑指向兄节点。 // 节点指的是xpr,对应3.1,3.2
节点无红自染红,然后向上再循环。 // 对应3.3
节点左红右旋转,色同父来右变黑。 // 对应3.4,3.5
最后一步父变黑,以父为轴左旋转。 // 对应3.6

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值