HashMap底层源码解析JDK1.8(附平衡二叉树、红黑树理论基础讲解)

JDK1.8 HashMap底层:红黑树与平衡二叉树的比较与应用
本文解析了HashMap在JDK1.8中如何利用红黑树优化插入/删除性能,对比了与平衡二叉树(如AVL)的差异,重点讲解了红黑树的插入规则和HashMap内部实现细节。

理论知识准备

平衡二叉树

对二叉排序树加以约束,要求每个结点的左右两个子树的高度差的绝对值不超过1,这样的二叉树称为平衡二叉树,同时要求每个结点的左右子树都是平衡二叉树,这样,就不会因为一边的疯狂增加导致失衡。

失衡情况包括以下四种:

左左失衡:通过右旋进行调整。

img

右右失衡:通过左旋进行调整。

img

左右失衡:先进行左旋,再进行右旋来调整。

img

右左失衡:先进行右旋,再进行左旋来调整。

img

通过以上四种情况的处理,最终得到维护平衡二叉树的算法。

红黑树

有了平衡二叉树,为什么还需要红黑树?

1、AVL的左右子树高度差不能超过1,每次进行插入、删除操作时,几乎都需要通过旋转操作保持平衡。
2、在频繁进行插入/删除的场景中,频繁的旋转操作使得AVL的性能大打折扣。
3、红黑树通过牺牲严格的平衡,换取插入/删除时少量的旋转操作,整体性能优于AVL。红黑树插入时的不平衡,不超过两次旋转就可以解决;删除时的不平衡,不超过三次旋转就能解决。

红黑树的特性:
(1)每个节点或者是黑色,或者是红色。(非黑即红)
(2)根节点是黑色。
(3)每个叶子节点都是黑色的空节点(NIL节点)。
(4)如果一个节点是红色的,则它的子节点必须是黑色的。(根到叶子的所有路径不可能存在两个连续的红色节点)
(5)从一个节点到该节点的叶子节点的所有路径上包含相同数目的黑节点。(相同的黑色高度)

约束4和5,保证了红黑树的大致平衡:根到叶子的所有路径中,最长路径不会超过最短路径的2倍。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TpE8UAiT-1650706957582)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\1650684698518.png)]

红黑树的基本插入规则

基本的插入规则和平衡二叉树一样,但是在插入后:
1. 将新插入的节点标记为红色。

情况1:父节点是黑色节点,直接插入,无需任何操作调整(不会打破上述规则)

情况2:插入的节点为根结点,则标记为黑色。

情况3:父节点是红色节点,需要调整(打破规则4)。

①叔节点为红色(叔父同色)
	①.1 将叔节点、父节点都标记为黑色
	①.2 祖父节点标记为红色
	①.3 然后把祖父节点当作插入节点进行分析

②叔节点为黑色(叔父异色)

    要分四种情况处理
    a.左左 (父节点是祖父节点的左孩子,并且插入节点是父节点的左孩子)
    进行右旋操作
    b.左右 (父节点是祖父节点的左孩子,并且插入节点是父节点的右孩子)
    进行左旋、再进行右旋操作
    c.右右 (父节点是祖父节点的右孩子,并且插入节点是父节点的右孩子)
    进行左旋操作
    d.右左 (父节点是祖父节点的右孩子,并且插入节点是父节点的左孩子)
    进行右旋、再进行左旋操作
    其实这种情况下处理就和的平衡二叉树一样。

    最后还是对于祖父节点重新进行分析。

分步和动画演示

向下面这棵树插入26;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C8tAfJN1-1650706957583)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\1650687907817.png)]
插入后26是红色,判断当前为情况3中的①,进行变色操作;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O6Whypys-1650706957584)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\1650687830838.png)]

变色操作后,找到26的祖父节点22,再重新进行分析。节点22满足情况3中的②中的c,就是右右的情况,因此需要进行左旋;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aMIwUU9I-1650706957585)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\1650687849552.png)]

左旋之后如下图所示,节点22的祖父节点为15(见上图),15作为插入节点变为红色;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EF2cqPt4-1650706957585)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\1650688235448.png)]

15变红色,父节点为根节点变成黑色,15没有祖父节点,红黑树插入流程结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KiKaFlfF-1650706957586)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\1650688000886.png)]

动图展示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MK81Q8g2-1650706957586)(C:\Users\86135\Desktop\redandblack2.gif)]

其他情况可以通过以下链接的动画演示,自行分析,或者参考维基百科的详细分析。

动画演示链接:(由于附上外部链接会被gf警告,需要动画演示链接可以私信获取!)

HashMap底层实现(JDK1.8)

在JDK1.8里面,HashMap是通过数组 + 链表 + 红黑树实现的,这部分的分析主要通过对源码的解读实现理解,具体看注释。

首先我们需要知道HashMap当中的成员变量。

基础参数

 	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

   /**
   * 必须是2的幂 <= 1<<30,即最大容量 为 2的30次方。
   */
   static final int MAXIMUM_CAPACITY = 1 << 30;

   /**
   * 构造函数中未指定时使用的负载系数(默认的加载因子)。
   */
   static final float DEFAULT_LOAD_FACTOR = 0.75f;
   
   /**
   * 将树而不是列表用于存储的存储单元计数阈值
   * 桶。当向至少有这么多节点的容器中添加元素时,容器将转换为树。大于2,* 该值必须并且至少应为8
   */
   static final int TREEIFY_THRESHOLD = 8;

   static final int UNTREEIFY_THRESHOLD = 6;

   /**
   * 可将链表转化成红黑树的数组的最小容量。
   *(否则,如果bin中的节点太多,则会调整表的大小。)
   * 应至少为4*TreeFiy_阈值,以避免调整大小和转化红黑树阈值之间的冲突。
   */
   static final int MIN_TREEIFY_CAPACITY = 64;

   /**
   * 此映射中包含的键值映射数。
   */
   transient int size;

   /**
   * 哈希桶,存放链表。长度是2的N次方,或者初始化时为0。
   */
   transient Node<K,V>[] table;

   transient int modCount;

   /**
    * 要调整大小的下一个大小值(容量 * 负载系数)
    * 哈希表内元素数量的阈值,当哈希表内元素数量超过阈值时,会发生扩容resize()
    */
   // javadoc描述在序列化时为真。
   // 此外,如果尚未分配表数组,则此字段将保留初始数组容量,或表示为零
   // 默认容量(初始容量)
   int threshold;

   /**
   * 哈希表的加载因子,用于计算哈希表元素数量的阈值。
   * threshold = 哈希桶.length * loadFactor
   */
   final float loadFactor;

put()方法

然后来看看最经常用到的put()⽅法

    public V put(K key, V value) {
   
   
        // 根据key计算hashcode,相对于JDK7中hash算法有所简化
        return putVal(hash(key), key, value, false, true);
    }

下面来看看hash方法,变量先保存了hashcode值,然后进行了一次异或运算。

这个异或运算可以近似看作是hashcode值的高位和低位进行的异或,为什么需要这样处理hashcode值呢?

这个主要跟JDK1.8中HashMap计算下标值有关,并不是通过对数组的大小取余进行计算,而是通过对(数组 - 1)进行与运算得到的一个下标值。提前处理hashcode值,得到结果是通过hashcode值高位和低位一起计算出来的,所以后续的与运算不只是是hashcode的低位有关,也就是这个下标(索引)值是由hashcode值的高位和低位一起决定的。

    /* ---------------- Static utilities -------------- */
    static final int hash(Object key) {
   
   
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

putVal()⽅法 ,注意这里还对putIfAbsent()方法实现了一定程度的维护。

putVal()⽅法中要点:

普通链表可能转化成红黑树Node -> TreeNode
单向链表会改造成双向链表,再变成红黑树,所以红黑树的结构既是红黑树,也是双向链表。
	// 如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value,用于实现putIfAbsent()方法。如果evict是false。那么表示是在初始化时调用的。
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
   
   
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        
        // 给tab赋值,并判断数组是否为null,如果是则初始化数组,并得到数组⼤⼩n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        
		// 根据hashcode计算出对应的数组下标i,并判断该位置是否存在元素
		// 下标i 是利用 哈希值 & 哈希桶的长度-1,替代模运算
 	    // 如果为null,则⽣成⼀个Node对象赋值到该数组位置
 	    // 否则,将该位置对应的元素取出来赋值给p
        if ((p = tab[i 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值