深度剖析HashMap

1. 概述

HashMap,由这个单词可以得到它的两个属性,Hash(基于哈希算法的哈希表结构存储数据)与Map(映射关系,以键值对,key-value形式存储),现在主流的是在jdk1.8以后的以红黑树为扩展结构的hash结构,红黑树又是一个重要的属性,它平衡了HashMap的查询性能。尽管HashMap可以使用大多数的场景,但总会有一些的多线程并发场景要用到Map又要使用hashmap特性(但HashMap并不是线程安全的),而ConcurrentHashMap是线程安全的,但它并不是用一个synchronize那么重量级和简单,所以其内部实现变的有趣而吸引人。

2. 哈希算法与哈希表

哈希算法,即散列算法,没有固定的实现方式,只要达到散列的目的即可,因此,哈希是一种算法思想,其本质就是要将数据分散的放到不同的有序(或者有某些逻辑属性的)集合单元内,以达到单元负载降低的目的。在不同的领域都可能会用到哈希,不单单是HashMap,比如分库分表的时候就要考虑均衡散列。

举个例子,就好比给坑里填萝卜,我们如果图省事的话可以把坑挖的特别大,然后一个坑里方n多个萝卜,尽管这样你只要走到到一个坑前(少走很多路,不用去其他坑)就把萝卜放完(当然,如果你仅仅只是单纯的想把萝卜放在坑里,你完全可以这样做),但是当有一天你需要把萝卜按照某些需要(比如按照大小,颜色,形状,质地派发给不同的泡面厂)得把萝卜取出来,实际上你也能办到,不过你就要花时间把萝卜挨个翻出来检查,符合条件的给它取出,大把的时间会耗费掉,当然,如果你又觉得时间不是问题也可以就这么干(换算到计算机术语就是没有性能要求)。如果你很珍惜时间的话,又比较勤快(换算到计算机术语就是由足够的空间),你就可以多跑点,把有相同或者相近属性的萝卜给它放在一个坑里,不同的就放在其他的坑,对于相同属性你必须放一块,没必要硬放在另一个坑,但相近的属性要有严格的规则来限制,不然你只要是红色的就放一块这样分散摆放的效果肯定会打折扣。这样,萝卜的整体摆放看起来更优雅也更合理。摆放完之后再给每个萝卜贴上标签,用个本子来记录这些标签的属性,再来找的时候是不是就一目了然了。贴的标签和记录属性的本子,其实就是哈希表。

哈希表就是键值码映射关系(key-value,感觉是不是跟HashMap结构差不多),我们会按照自己的需要定义一个哈希表(key是固定的类型和结构,它往往标识着一个确定的哈希表的位置标码),通过哈希算法将key转换成哈希表认可的key结构(这个算法往往需要提取key的属性通过转换,计算,移位等获取有关联的尽可能唯一结果),从而形成看似有效的映射关系(因为哈希表的集合要么是有序,要么是由逻辑关联属性,能够快速定位到具体数据,取数据的时间为O(1),比如数组,HashMap就是用的数组,也不单纯就是数组)。

然而,算法都是针对一些特定属性来进行hash, 那么就必然存在这样一种情况,在对某一key值进行哈希散列的时候,先前有一个key1具备与key相同的有效属性(即哈希算法要用到的key的特征属性),那么哈希算出来的值就一样了,两个值存在在一个坑里,显然就需要有一个机制来进行决策,用哪一个,不可避免的需要引入附加属性和扩展逻辑来完善系统逻辑。这个就是哈希表的冲突问题及解决方案。

HashMap的附加属性和扩展逻辑就是对具有相同hash值的对象进行链式存储,再通过额外属性(对象的值是否一致)来筛选,这样看起来是能解决几乎所有的问题,除了事物变化之外(比如日趋频繁的交互对响应时间的要求越来越高,要求程序内部的执行时间尽可能的快)。我们都知道,链表的查询速度是很慢的,链式结构的引用嵌套检索在链路变长了之后,其性能将会骤降。

考虑到这一变化,在HashMap中引入了树结构来解决,而考虑到树结构本身带来的性能损耗,只有当数据集合达到一定数量且哈希表中存在的链式结构的深度达到一定的长度时,才使用树结构来存储,集合的数量和链式结构的深度值需要经过验证最终得到平衡值来区分是否要使用树结构进而使查询和存放的效率最大化。这里引入的树结构就是红黑树

3. 红黑树与二叉树

红黑树是二叉树的扩展,二叉树有诸多的子集,红黑树,霍夫曼树,二叉排序树等,今天单独讨论红黑树与二叉树这个分支。

3.1 二叉树

二叉树是指每个节点最多有两个子节点,即每个节点的度不大于2,左子节点与由子节点及其递归节点(树)有序(有序就为快速检索提供了可能),顺序不能颠倒,即使二叉树的节点只有一个,也要根据顺序将其划分为左子节点或右子节点,根据其节点形态又可分为完全二叉树(所有叶子节点集中在树的左侧),满二叉树(除最后一层外每一层都有两个子节点)。二叉树具备在理想情况下的优秀检索能力,但仍然有一定几率会出现构造树时,节点基本有序,造成数据大部分在根节点的左侧或者右侧,甚至极端情况下,会将二叉树转换成链表(完全有序),这显然是不能接受的。那么要用二叉树的特性,又想避免掉这种情况的出现,就衍生出一些自平衡树(自带一些转换属性,强制将树的节点尽可能的均匀分布在两侧,靠近满二叉树,以增强其操作效率),红黑树便是其中的一种。

3.2 红黑树

红黑树相较于二叉树,从本质上来说就多了一套自平衡机制(设置一些强制条件集合A,使得从表现上来看,具备A的树结构具有子节点动态平衡于父节点的两侧,具备A条件的树结构称为红黑树【或者对称二叉B树】,当不满足A条件时进行某些转换进行调整(故为动态平衡),因为其动态平衡性,使得类似二分查找时能将结果在尽可能少的二分次数内定位到标的数据,在庞大数据量时,这提供了较高的查询体验),是一种常用的二叉查找树方案。

来看看红黑树应具备的条件(强制性条件集合A):
1.附加节点的颜色属性,有且仅有red(红)或者black(黑)两种;
2.根节点必须为黑色;
3.每个叶子节点都是黑色(黑色节点可为空,叶子节点都是NULL);
4.如果一个节点是红色的,那么它的叶子节点必须是黑色的;
5.从某节点到该节点的子孙节点的所有路径上包含相同数量的黑色节点

因为红黑树是红黑节点每一条路径只有红色和黑色,进而结合第(5)点保证任意路径的深度不会比最短的路径深度高出2倍,再结合合理的翻转,旋转,转换机制,使得红黑树具有接近平衡树的结构,这对于不可预期量级的数据集来说二叉树检索效率就会更高。一棵含有n个节点的红黑树的高度至多为2log(n+1)(不展示具体论证过程)

要说红黑树,就不得不说树的三种操作:插入查询删除
查询:其中查询不涉及树结构的变更,因而是最简单的,按照一致的比较算法(跟插入和删除使用的比较逻辑一致)二叉树检索得到目标结果。

至于插入和删除可以有不同的实现,原则上来说,插入和删除带来的结构变化,要通过左旋或者右旋实现红黑树的基本平衡,不同的架构可能存在不同的实现,但思路和逻辑是一致的。

4. HashMap

4.1 hash散列

HashMap是使用key-value形式存储数据的一种数据结构,我们通过唯一的key来对 对应数据对象快速的定位获取,在数据充分散列且数据规模较小的情况下(仅仅用数组存储,经过 (n - 1) & hash算出下标),HashMap中这个时间复杂度为O(1)。为了将数据对象均匀的放到数组的不同位置里,(n - 1) & hash就是均匀分布的核心所在,有以下2点需要注意:
首先,hash代表的是key对象的hash值(整数),jvm底层散列算法已经使得碰撞概率极低,并且HashMap在计算key的hash值也并不是照搬原生的hashCode方法,进行了如下转换(通过右移加异或的双重处理降低不同hash散列值二进制低位重复出现的概率):

//计算key对象的hash值
//将对象的hashCode值右移16位后与原hashcode值按位异或
//上面也说了,hash值会向数组长度靠拢,然而hash值40多亿的范围导致hash值会取到低位
//右移将高位的差异转嫁到低位,然后通过异或将这种变化赋予给新的hash值
//故而通过这种右移加异或的双重处理降低不同hash散列值二进制低位重复出现的概率
static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

其次,hash的取值范围在-2147483648 ~ 2147483648,40多亿,内存扛不住那么大的范围,故而HashMap的数组的最大长度(MAXIMUM_CAPACITY)在1073741824(1 << 30),因此需要将hash散列值转换到数组长度范围内,而HashMap抛弃了常用的a % b进行取模运算,进而使用加以限制却更加高效的二进制位运算,与运算。(n - 1) & hash要达到取模的目的,必须要求n是2的幂(这个就是前文所说的限制)。

(n - 1) & hash中n为2的幂,意味着n的二进制表达式为1000000000…0,n-1就是000000111111…1,这种情况下,hash不大于n-1的位都会被保留,达到取余数的效果。在put,get,remove和扩容的时候,会用到这个方法,涉及到定位数据。

HashMap是如何针对指定capacity(容量)保证数组的length一定是2的幂呢?通过分次右移capacity,使得最终的capacity满足:容量是比初始容量大的最小2的幂。这样既满足二次幂,又使得容量尽可能的小,节省内存,其代码如下:

/**
* 1. 可以发现,n一直在做 |= 运算,这使得cap的二进制一直从高位开始不断变为1
* 2. 而右移的位数合起来正好是31(1+2+4+8+16),已经覆盖了最大容量
* 3. cap-1的目的是为了避免cap正好是2的幂时,右移之后成为原来的2倍,造成了空间资源
*    的浪费,如果是非2的幂,cap-1不影响获取最小2次幂
**/
static final int tableSizeFor(int cap) {
     int n = cap - 1;
     n |= n >>> 1;
     n |= n >>> 2;
     n |= n >>> 4;
     n |= n >>> 8;
     n |= n >>> 16;
     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

4.2 HashMap put数据

插入数据的逻辑是,如果key的最终hash值唯一,则直接作为数组元素存储(在知道索引值的情况下复杂度为O(1)),如果hash值重复但重复数量不多时使用链表存储(O(n)),当重复数到底一定阈值的时候使用红黑树(log(n))。

插入数据操作如下(结合代码解析插入步骤):


	//加载系数,为浮点数,扩容时要用
	final float loadFactor;
	//扩容临界值,其值为:容量 * 加载系数
	int threshold;
	//默认容量,必须为2的幂,这与HashMap的散列算法有关
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab:数组容器,p:存放hash节点
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        	//如果数组为空,对其进行初始化
        	//调用统一的resize(),是一个公用的扩容算法
        	//resize()的逻辑是,判断数据数组(table)是否为空
        	//若为空,就对其进行初始化容量和扩容阈值
        	//若不为空,将容量和扩容阈值翻倍,并进行数组拷贝
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//若散列槽内没有数据,创建节点数据,链式节点
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //如果hash值和key对象一致,说明对同一个key put
                e = p;
            else if (p instanceof TreeNode)
            	//若散列槽节点为树节点,说明这个槽内的数据已经树化,要使用红黑树的插入算法
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                	//若散列槽有数据但不为树节点,进行链式插入
                    if ((e = p.next) == null) {
                    	//找到链表的最后一个节点,进行尾部插入
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                        	//链表长度如果大于8,则将链表转化为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //若在链表内找到对应key值,则不再查找,因为不必重新插入
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                	//当不能重复插入表示为false或者原数据为空,进行值替换
                    e.value = value;
                afterNodeAccess(e);
                //如果插入成功,则直接返回,因为是链式结构或树结构,所以不会造成size的增加
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
        	//如果size超过当前容量的0.75(也可自定义),则进行扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

当put数据到同一个hash槽,并且链表长度超过8时,就要将这一个hash槽内的链表转换成红黑树来存储。虽然红黑树有可靠(自平衡)且良好(二叉检索)的查询性能,但是其插入和删除节点会带来一定的开销和复杂度,多了一倍的引用关系(多了一倍空间损耗),而且为了维持红黑树的特征,在插入和删除时要做额外的运算逻辑和更为复杂的结构变换,非必要不会用红黑树。

之所以树化阈值会用8:

  1. 首先,是出于链表和红黑树的查找平均复杂度比较得出,数据规模为n时,链表的查找平均时间复杂度为n/2,红黑树为log(n)(且比较稳定),当n=8时,n/2=4, log(8)=4,可见,两者平均时间复杂度相等,往后面走红黑树的优势只会越来越明显,也就是只要链表规模大于8转换红黑树是划算的。
  2. 其次,要考虑的是,就算链表长度不小于8,转红黑树依旧要考虑一个问题,构造树和旋转树带来的额外复杂度,如果链表长度达到8左右的概率达到峰值,而其他的的值出现概率很低,就会造成树结构和链表结构的频繁转换,但链表长度出现的频率符合泊松分布,并给出了如下的分布规律:
    /* * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
	*/

可见链表长度达到8的概率为0.00000006,已经很低了,并且随着链表长度基数增大,这个概率只会越来越低。

链表长度不小于8是前提条件,但是hash表的长度如果过低,很容易就造成数据堆叠到一个hash桶内,我们知道hash值计算的时候是取的小于hash表长度的模,长度越低,链表的长度就可能越长,就越容易树化。想一种极端的情况,hash表初始化容量为16,当其扩容一次为32后,要写入32*0.75=24条数据,其中有8条恰巧进入同一个hash桶内,这个时候就要进行树化的话,总共24条数据就有8条挂在红黑树上,不合理。

另一方面,hash表的长度必须是大于(4 * 最小树化链表长度)以避免扩容和树化的冲突

故而链表树化不能在hash表长度小于某个值时进行,应当寻找一个合适的长度阈值,使得使用树结构更合理,这个阈值就是MIN_TREEIFY_CAPACITY = 64。

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        	//hash表为空或者数组长度过小,进行扩容
        	//扩容操作包含初始化和hash表长度翻倍,初始化使用HashMap的默认配置,重点是在已有数据的hash表长度翻倍
        	//因为cap = cap * 2,链表或者树内的节点会分布在原位置n或者n+cap上,扩容之后高位可能为1或者0,故而造成这种分布
            resize();
        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);
            if ((tab[index] = hd) != null)
            	//将链表进行红黑树结构变换
                hd.treeify(tab);
        }
    }

put操作当符合树化操作后,对头节点进行红黑树插入:

final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            //遍历链表,将其数据逐个放置到红黑树的正确位置
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                //如果根节点为空,将入参节点作为根节点,并置为黑色
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    //对根节点进行遍历,按照二叉树规则放置
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        //首先根据hash code作为二叉树排序规则,比红黑树节点hash小的放在左树,否则放在右树
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        //如果根据hashcode无法判断(即hash相等),则根据k的对象是否实现了
                        //cpmpareable接口或者具备compare的能力,则调用compare方法获取
                        //二叉树顺序
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        //这里是递归的关键,根据二叉树顺序获取对应的子树,如果为空则将当前
                        //遍历的链表节点放置到红黑树
                        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;
                        }
                    }
                }
            }
            //红黑树经过左旋和右旋之后,根节点可能会变更,为了查询时第一时间就能定位到根节点
            //并进行二叉树检索,将红黑树节点放置到hash桶头位置
            moveRootToFront(tab, root);
        }

红黑树添加节点,其关键点在于新增树节点之后的树平衡处理,根据不同的情况进行不同的处理,详情见注释:

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
            //红黑树任何一个新增的节点,初始颜色均为红色,因为红黑树的规则规定的是任意路径的
            //黑色节点数相同,如果新增节点初始颜色为黑色,则会增加红黑树新增或删除时左旋和右旋
            //的可能性,若为红色节点则更好处理
            x.red = true;
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
            	//父节点为空,那么将新增节点置为根节点,并将其变为黑色
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                //如果新增节点的父节点为黑色,说明不用考虑新增节点变色的问题,能保持原有平衡
                //如果祖父节点为空,说明父节点是根节点,必定为黑色,故而也不用做平衡处理
                //直接返回根节点,平衡处理结束
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                //父亲节点是红色,也就意味着新增节点的分支路径会多出一个黑色节点
                //违反红黑树定义5:所有路径的黑色节点数量必须一致
                //因此需要对红黑树进行平衡处理
                //这种情况下,祖父节点必定为黑色,父亲节点为红色
                //分左右子节点做不同处理,但处理是对称的,以下为左子节点的平衡处理
                if (xp == (xppl = xpp.left)) {
                	//如果父节点的兄弟节点不为空且为红色
                	//这种情况,如果仅仅只是在当前单一分支进行处理难以达到树的再平衡
                	//需要将父节点及其兄弟节点置为黑色,将其祖父节点置为红色
                	//这样,新增节点为红色就不需要变色,同时红黑树也维持了原有的黑节点数
                	//整个树还是处于平衡中,不需要调整树结构
                	//但因为祖父节点置为红色,前辈节点 可能不符合红黑树定义
                	//同时祖父节点变为红色,细想是不是跟新增节点为红色殊途同归
                	//想当于把分支节点的变色处理转嫁到前辈节点,以达到红黑树的最终平衡
                    if ((xppr = xpp.right) != null && xppr.red) {
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    //如果叔伯节点为空或者为非空黑色。也就是不能通过置黑操作保证统一祖父节点
                    //这一路径所有分支的黑色节点数统一,这种情况就需要对左子树进行调整
                    else {
                        if (x == xp.right) {
                        	//若新增节点为为父节点右子节点,对其父节点进行左旋
                        	//这样就形成了祖父节点(黑)——新增节点(红)——父节点(红)左路径(即依次为后序的左子节点)
                        	//这样做的原因是,叔伯节点为黑色,不能通过改变祖父节点路径下的
                        	//颜色内部消化(达到平衡),这样做能为叔伯节点增加一个节点做铺垫
                        	//从而使得祖父节点的所有树路径都黑节点数维持平衡。
                        	//如果新增节点为父节点的左子节点,那么其本就在祖父节点——父节点
                        	//——新增节点,左子节点路径中,故而不需要进行左旋
                            root = rotateLeft(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                        	//父节点不为空,将父节点置为黑色
                        	//如果
                            xp.red = false;
                            if (xpp != null) {
                            	//祖父节点不为空,将祖父节点置为红色,并对祖父节点进行右旋
                            	//祖父节点变成父节点的右子节点,这样曾祖节点到变更路径的黑
                            	//色节点数量维持了原有的平衡,红黑树再次达到平衡
                                xpp.red = true;
                                root = rotateRight(root, xpp);
                            }
                        }
                    }
                }
                else {
                	//如果父节点为祖父节点的右子节点,则看叔伯节点是否为红色
                    if (xppl != null && xppl.red) {
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                    	//与第一种情况对称处理,进行右旋
                        if (x == xp.left) {
                            root = rotateRight(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                        	//与第一种情况进行对称处理,进行左旋,并置黑父节点
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateLeft(root, xpp);
                            }
                        }
                    }
                }
            }
        }

4.3 HashMap 删除数据

删除数据也分多种情况,hash表删除,链表删除,红黑树删除,前两者的删除较为简单,直接将链表节点对象的后继节点往前移,允许空节点:

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;
        //对应key在hash表中有值则继续
        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;
            //与hash表里的对应元素进行比较,hash值,key的值以及对象引用对比,得出相等,则取该
            //值对应的value作为值对象
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
            	//如果与头节点不一致,则判断是否为树节点,进行二叉检索,找到对应的节点
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                	//如果不是树结构,则对链表进行遍历,找出对应节点
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //能够删除的条件:
            //1.不能删除空节点
            //2.可选的传入要删除的对象的引用,与hashMap中的值进行比较,这样做的目的是,对于
            //  敏感数据,尽可能的避免误删
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                	//如果待删除的对象是树节点,执行树删除,这可能会有额外的树平衡操作
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                	//如果删除的节点为链表的首节点,那么直接将该首节点指向其下一节点
                    tab[index] = node.next;
                else
                	//如果不是首节点,处于链表的中间节点,则将后续节点前移
                    p.next = node.next;
                //操作次数计数
                ++modCount;
                //hashmap成员数减少
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

删除操作的核心复杂点在于,红黑树节点的删除,尽管其出现的概率低,但其复杂度却相对较高,删除之时需要先将链式关系变更,然后进行树结构变更,树结构的删除如下:

//这个是封装在TreeNode里的方法,因此通过this关键字就能得到要删除的节点引用
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
            int n;
            if (tab == null || (n = tab.length) == 0)
            	//hash表不能为空
                return;
            //计算key在hash桶中的位置,与插入数据的规则一致,取小于容量的任意值
            int index = (n - 1) & hash;
            //定义根节点与第一个节点,这里的根节点就是第一个节点
            TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
            //获取下一节点,注意,这里的next和prev是HashMap的Node,是树继承而来的属性
            TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
            if (pred == null)
            	//如果删除节点的前置节点为空,则说明要删除的节点为根节点
                tab[index] = first = succ;
            else
                pred.next = succ;
            //设置后置节点的前置节点为删除节点的前置节点,无论是否为空
            if (succ != null)
                succ.prev = pred;
            //如果链表层次的删除执行后的hash表首节点为空,则直接退出
            if (first == null)
                return;
            //若首节点存在非空父节点,那么就要去对root节点重新取用
            //此时就算删除节点为根节点,其在内存中依旧存在
            if (root.parent != null)
                root = root.root();
            if (root == null
                || (movable
                    && (root.right == null
                        || (rl = root.left) == null
                        || rl.left == null))) {
                //当树结构太小,就将其退化为链表
                tab[index] = first.untreeify(map);  // too small
                return;
            }
            //当该节点所在数据结构还能维持树结构,则进行树删除节点逻辑
            //首先要做的是删除后由哪个节点来顶替这个节点的位置,同时又要保持红黑树的
            //平衡,所以在删除后要进行再平衡处理,当然也会考虑红黑树退化的情况
            TreeNode<K,V> p = this, pl = left, pr = right, replacement;
            //若删除节点左右子节点存在空节点时,那么他的后继节点是固定的,情况如下:
            //1.左空右非空,那么右子节点为后继者
            //2.做非空右空,那么左子节点为后继者
            //3.两者都为空,则直接删除,交给前辈节点去让他们重新平衡
            //4.左右子节点都不为空,则进行下面逻辑
            if (pl != null && pr != null) {
            	//左右子节点均不为空,寻找替换节点,首先找到被删除节点的右子节点
            	//默认为其右子节点,然后递归找其最左节点,该节点的树位置将是被删除
            	//的对象
                TreeNode<K,V> s = pr, sl;
                while ((sl = s.left) != null)
                    s = sl;
                //删除节点与大于该节点的最小节点颜色互换
                boolean c = s.red; s.red = p.red; p.red = c; 
                TreeNode<K,V> sr = s.right;
                TreeNode<K,V> pp = p.parent;
                if (s == pr) {
                	//若删除节点的替换节点为其右节点,直接将两者交换
                    p.parent = s;
                    s.right = p;
                }
                else {
                	//若删除节点与其有节点不一致,位置引用关系互换
                    TreeNode<K,V> sp = s.parent;
                    if ((p.parent = sp) != null) {
                        if (s == sp.left)
                            sp.left = p;
                        else
                            sp.right = p;
                    }
                    if ((s.right = pr) != null)
                        pr.parent = s;
                }
                //删除节点左节点置空
                p.left = null;
                //删除位置有节点引用关系变更
                if ((p.right = sr) != null)
                    sr.parent = p;
                //删除节点左节点
                if ((s.left = pl) != null)
                    pl.parent = s;
                //如果删除节点原位置为根节点,将替换节点设置为根节点
                //若不是根节点,则原位置的父节点对其的引用改为替换节点
                if ((s.parent = pp) == null)
                    root = s;
                else if (p == pp.left)
                    pp.left = s;
                else
                    pp.right = s;
                //首先,sr不可能为非空的黑色节点,这样是不满足从根节点到叶子节点的黑节点数量一致
                //那么只可能存在一个深度最高为2的子树(红色节点)
                //当其为非空时,只能为红色,且子节点为空
                //那么删除位置就应该在替换节点和其右子节点(非空)当中选一个
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            //删除节点左右子树存在空值时
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                replacement = p;
            if (replacement != p) {
            	//当删除节点的子节点存在非空节点时,将其树关系转嫁到与其替换的节点
            	//与此同时,将删除节点的树关系置为空,当引用关系为空,那会被GC自动回收
            	//以达到删除该节点的目的
                TreeNode<K,V> pp = replacement.parent = p.parent;
                if (pp == null)
                    root = replacement;
                else if (p == pp.left)
                    pp.left = replacement;
                else
                    pp.right = replacement;
                p.left = p.right = p.parent = null;
            }
			//当确定要要删除的树位置后,需要对树进行删除后再平衡处理,使其再次满足红黑树特性
            TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
			//当删除节点没有非空子节点时,删除其自身
            if (replacement == p) {  // detach
                TreeNode<K,V> pp = p.parent;
                p.parent = null;
                if (pp != null) {
                    if (p == pp.left)
                        pp.left = null;
                    else if (p == pp.right)
                        pp.right = null;
                }
            }
            //将新的根节点放入到hash桶内
            if (movable)
                moveRootToFront(tab, r);
        }

红黑树状态下删除节点,因为不确定删除位置,会给再平衡造成困扰,hashMap采取寻找大于它的最小节点(在此之前先对节点进行替换),降低了删除再平衡的复杂度,那么删除节点的情况就可以穷举,如下:

  1. 删除节点为红色,那么删除它对于整个树的平衡没有影响,不需要执行再平衡,即进行再平衡的都是黑色节点;
  2. 删除节点为黑色,且兄弟节点为红色,这种情况下,父节点必定为黑色节点,此时对父节点进行左旋,

其删除再平衡 处理代码如下:

static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
                                                   TreeNode<K,V> x) {
            //同时只有被删除节点的颜色为黑色才需要进行这个平衡操作,那么也就意味着,被删除位置
            //所能影响的树路径将会减少一个黑色节点,由叶子节点逐层向上,直至有可能在路径新增一个
            //黑色节点(前提时不破坏红黑树的定义),
            for (TreeNode<K,V> xp, xpl, xpr;;) {
            	//当循环到空或者根节点时,终止遍历
                if (x == null || x == root)
                    return root;
                //当遍历到根节点时,变色并终止
                else if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                //首先,当遍历到红色节点,
                else if (x.red) {
                    x.red = false;
                    return root;
                }
                
                else if ((xpl = xp.left) == x) {
                    if ((xpr = xp.right) != null && xpr.red) {
                        xpr.red = false;
                        xp.red = true;
                        root = rotateLeft(root, xp);
                        xpr = (xp = x.parent) == null ? null : xp.right;
                    }
                    if (xpr == null)
                        x = xp;
                    else {
                        TreeNode<K,V> sl = xpr.left, sr = xpr.right;
                        if ((sr == null || !sr.red) &&
                            (sl == null || !sl.red)) {
                            xpr.red = true;
                            x = xp;
                        }
                        else {
                            if (sr == null || !sr.red) {
                                if (sl != null)
                                    sl.red = false;
                                xpr.red = true;
                                root = rotateRight(root, xpr);
                                xpr = (xp = x.parent) == null ?
                                    null : xp.right;
                            }
                            if (xpr != null) {
                                xpr.red = (xp == null) ? false : xp.red;
                                if ((sr = xpr.right) != null)
                                    sr.red = false;
                            }
                            if (xp != null) {
                                xp.red = false;
                                root = rotateLeft(root, xp);
                            }
                            x = root;
                        }
                    }
                }
                else { // symmetric
                    if (xpl != null && xpl.red) {
                        xpl.red = false;
                        xp.red = true;
                        root = rotateRight(root, xp);
                        xpl = (xp = x.parent) == null ? null : xp.left;
                    }
                    if (xpl == null)
                        x = xp;
                    else {
                        TreeNode<K,V> sl = xpl.left, sr = xpl.right;
                        if ((sl == null || !sl.red) &&
                            (sr == null || !sr.red)) {
                            xpl.red = true;
                            x = xp;
                        }
                        else {
                            if (sl == null || !sl.red) {
                                if (sr != null)
                                    sr.red = false;
                                xpl.red = true;
                                root = rotateLeft(root, xpl);
                                xpl = (xp = x.parent) == null ?
                                    null : xp.left;
                            }
                            if (xpl != null) {
                                xpl.red = (xp == null) ? false : xp.red;
                                if ((sl = xpl.left) != null)
                                    sl.red = false;
                            }
                            if (xp != null) {
                                xp.red = false;
                                root = rotateRight(root, xp);
                            }
                            x = root;
                        }
                    }
                }
            }
        }

未完待续。。。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值