首页 > 编程语言 >java集合之Map篇——HashMap(底层源码非常详细说明)

java集合之Map篇——HashMap(底层源码非常详细说明)

时间:2024-07-26 22:55:46浏览次数:15  
标签:Map java next 链表 源码 哈希 xp null 节点

前言

前面先做了红黑树的讲解平衡二叉树和红黑树-CSDN博客,就是为了为了Map集合做铺垫,Map的几个实现集合底层都用到了红黑树。由于HashMap的东西有点多,HashTable和TreeMap下篇再说明。

一、HashMap

hashMap底层是哈希表+哈希桶(数组或红黑树)

5272764ff8f8442aa90ebf10bf622b94.png

 Set篇的几张图会漂亮一点

1.几个重要的内部类

(1)节点类型

a4e05517e41d4188bea87c615c18ce37.png

下面是两个节点内部类的源码:

链表节点类,最基本存储数据的节点类型。 

50726fc5d61f46ca934220c1a70b71da.png

 树节点类,树化后的哈希桶的节点的类型

822e6658cda949efabf9a529e27e956e.png

(2)利于遍历的内部类

    // 定义一个内部类EntrySet,继承自AbstractSet,泛型为Map.Entry<K,V>
    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {

        // 返回EntrySet中元素的数量,即HashMap的大小
        public final int size()                 { return size; }

        // 清空EntrySet中的所有元素,实际调用HashMap的clear方法清空整个Map
        public final void clear()               { HashMap.this.clear(); }

        // 返回一个迭代器,用于遍历EntrySet中的所有元素
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }

        // 检查指定对象是否在EntrySet中存在
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))       // 如果传入的对象不是Map.Entry类型,直接返回false
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;   // 将传入的对象转换为Map.Entry类型
            Object key = e.getKey();             // 获取Map.Entry的key
            Node<K,V> candidate = getNode(hash(key), key); // 通过hash和key获取候选节点
            return candidate != null && candidate.equals(e); // 判断候选节点是否等于传入的Map.Entry对象
        }

        // 移除EntrySet中指定的对象,如果成功移除则返回true
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {          // 如果传入的对象是Map.Entry类型
                Map.Entry<?,?> e = (Map.Entry<?,?>) o;   // 将传入的对象转换为Map.Entry类型
                Object key = e.getKey();           // 获取Map.Entry的key
                Object value = e.getValue();       // 获取Map.Entry的value
                return removeNode(hash(key), key, value, true, true) != null; // 通过hash、key和value尝试移除节点,如果成功返回true
            }
            return false;                          // 如果传入的对象不是Map.Entry类型,返回false
        }

        // 返回一个Spliterator,用于分割并行处理EntrySet中的元素
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }

        // 对EntrySet中的每个元素执行指定的操作
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;                      // 创建Node数组引用tab
            if (action == null)                   // 如果传入的操作action为null,抛出NullPointerException异常
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) { // 如果EntrySet的大小大于0且table不为空
                int mc = modCount;                // 记录当前的修改次数
                for (int i = 0; i < tab.length; ++i) { // 遍历table数组
                    for (Node<K,V> e = tab[i]; e != null; e = e.next) // 遍历table数组中的链表
                        action.accept(e);              // 对链表中的每个元素执行action操作
                }
                if (modCount != mc)               // 如果在执行过程中有其他线程修改了EntrySet
                    throw new ConcurrentModificationException(); // 抛出ConcurrentModificationException异常
            }
        }
    }

 EntrySet这个内部类的作用:方便遍历,取键值

A. k-v 最后是 HashMap$Node node =newNode(hash,key,value,null)

B. k-v为了方便程序员的遍历,还会创建EntrySet集合,该集合存放的元素的类型Entry,而一个Entry。

对象就有k,v    EntrySet<Entry<K,V>>即:transient Set<Map.Entry<K,V>> entrySet;

C.   entrySet中,定义的类型是Map.Entry,但是实际上存放的还是HashMap$Node

这是因为 static class Node<K,V>implements Map.Entry<K,V>

D. 当把HashMap$Node对象存放到entrySet就方便我们的遍历,因为Map.Entry提供了重要方法

 K  getKey();   V   getValue();

2.几个重要的变量和常量

 哈希表即节点类型数组

cccaf14978e948d89d2921a4e8cbfdfd.png

默认初始容量16 

 d2e396b47b634827b8dda4b0eb894d7d.png

默认的加载因子0.75         

20fe193eb2f44094a7c840fa31893ea3.png

哈希表的最大容量 2^30

792a1b1d16024f1f982aa0fe5be73412.png

哈希桶树化的链表临界值8(达到这个值8还要哈希表达到最小树化容量64) 

495dffddaf524a9d8e312f7405d15ae7.png

树化的另外一个条件哈希表达到最小树化容量64

97952ae2b41847d8a9ba97de50b3df87.png

3.几个重要的方法

添加元素时分三种情况:

1.数组位置为null

2.数组位置不为null,键不重复,挂在下面形成链表或者红黑树

3.数组位置不为null,键重复,元素覆盖

(1)put()方法

//参数一:键
//参数二:值

//返回值:被覆盖元素的值,如果没有覆盖,返回null
   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

(2)hash()方法

//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(3)putVal()方法

//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
//        true,表示老元素的值保留,不会覆盖
//        false,表示老元素的值不保留,会进行覆盖
//参数五:会传入一个空方法没有任何意义
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //定义一个局部变量,用来记录哈希表中数组的地址值。
        Node<K,V>[] tab; 

        //临时的第三方变量,用来记录键值对对象的地址值
        Node<K,V> p;

        //表示当前数组的长度    
        int n;

        //表示索引
        int i;

        //先将哈希表中的数组的地址值,赋值给局部变量tab
        if ((tab = table) == null || (n = tab.length) == 0)
            //1.如果当前是第一次添加数据,底层会创建一个默认长度为16,加载因子为0.75的数组
            //2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
            //如果没有达到扩容条件,底层不会做任何操作
            //如果达到了扩容条件,底层会把数组扩容为原先的两倍,并把数据全部转移到新的哈希表中
            n = (tab = resize()).length;//表示把当前数组的长度赋值给n
        //拿着数组的长度跟键的哈希值进行计算,计算出当前键值对对象,在数组中应存入的位置
        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))))
                e = p;
            else if (p instanceof TreeNode)
                //判断数组中获取出来的键值对是不是红黑树中的节点
                //如果是,则调用方法putTreeVal,把当前的节点按照红黑树的规则添加到树当中。
                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);
                        //判断当前链表长度是否超过8,如果超过8,就会调用方法treeifyBin
                        //treeifyBin方法的底层还会继续判断
                        //判断数组的长度是否大于等于64
                        //如果同时满足这两个条件,就会把这个链表转成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //链表里面出现相同的键,就会中断遍历链表。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
          
            //只有数组或链表出现相同的键,才会执行这部分模块,覆盖原来键的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    //重复键对应的值替换旧的值
                    e.value = value;
                afterNodeAccess(e);//空方法
                return oldValue;
            }
        }
        ++modCount;
        //thresho1d:记录的就是数组的长度*0.75,哈希表的扩容时机16*0.75=12
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);//空方法

        //表示当前没有覆盖任何元素,返回null
        return null;
    }

(4)resize()

    /**
     * 初始化或翻倍哈希表的大小。如果当前表为空(null),则根据threshold字段中保存的初始容量目标进行分配。
     * 否则,将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通链表节点,则节点按原顺序进行分组。
     *
     * @return 新的哈希表
     */
    final Node<K,V>[] resize() {
        //将table给临时变量oldTab
        Node<K,V>[] oldTab = table;
        // 现有容量的大小,等于数组的长度,如果数组为空,返回0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 现有的扩容阈值
        int oldThr = threshold;
        // newCap表示新的容量,newThr新的扩容阈值
        int newCap, newThr = 0;
        // 如果现有容量大于0,表示已经初始化过了
        if (oldCap > 0) {
            // 如果现有容量已经大于最大容量。结束扩容,直接返回
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
             // 否则,如果扩大两倍之后的容量小于最大容量,且现有容量大于等于初始容量16    
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                 // 新的扩容阀值扩大为两倍,左移<<1 相当于乘以2
                newThr = oldThr << 1; // double threshold
        }
        // 否则如果当前容量等于0 ,但是当前扩容阈值 > 0,调用有参构造函数会到这里
        else if (oldThr > 0) // initial capacity was placed in threshold 
            //这是带参构造器创建的集合才会进入到这里
             // 进入这里,新的容量等于当前的扩容阈值,
            newCap = oldThr;
        // 否则如果当前容量等于0
        else {               
            // 新的容量等于默认容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 新的扩容阈值等于默认负载因子0.75*默认容量16=12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果新的扩容阈值等于0
        if (newThr == 0) {
            // 设置新的扩容阈值等于新的容量*负载因子
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
       // 设置hashmap对象的扩容阈值为新的扩容阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 初始化数组     
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // 设置hashmap对象的桶数组为newTab
        table = newTab;
        // 下面是rehash的过程
        // 如果旧的桶数组不为空,则遍历桶数组,并将键值对映射到新的桶数组中
        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)
                        // 用新数组容量取模,设置到新数组中
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果节点是红黑树    
                    else if (e instanceof 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;
                        //当前节点的下一节点
                        Node<K,V> next;
                         // 遍历链表,并将链表节点按原顺序根据高低位分组
                        do {
                            next = e.next;
                            //通过e.hash & oldCap算出当前节点是0放低位,1放高位
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else

                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                         // 将分组后的链表映射到新桶中
                         //低位链表放在新哈希表的原来桶的位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //高位链表放在新哈希表倍增的对应原来桶的位置
                        //因为扩容是*2,所以新区域对应映射的位置是原位置+旧容量
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
}

(5)split()方法

对当前哈希桶的红黑树进行拆分拷贝到新的哈希桶中。

        /**
         * 将树形二进制箱中的节点分割成低和高两个树形二进制箱,
         * 或者如果现在太小则进行非树化。仅在调整大小时调用;
         * 参见上面关于分割位和索引的讨论。
         *
         * @param map 当前的映射表  扩容后拷贝旧哈希表拷贝一半的集合
         * @param tab 扩容后的新数组   扩容后的新哈希表  也是拷贝一半
         * @param index 正在分割的表格的索引   原树根在旧哈希表的索引
         * @param bit 哈希值用于分割的位  旧哈希表的容量    旧哈希表的容量
         */

        //原本树化的时候红黑树的节点之间还是维护着一个双向链表的
        //所以拷贝可以通过双链表拷贝,
        //不过遍历双链表时,不同节点的在扩容后的新哈希表的位置重新计算
        //分为高位和低位,跟拷贝单链表一样
        //不同的是  高位或者低位链表<=阈值8 就会进行非树化
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            // 从当前节点开始,迭代链中的所有节点
            //this指的要拷贝的红黑树的树根
            TreeNode<K,V> b = this;

            // 初始化低和高的头部和尾部节点,用于区分哈希值中特定位为0和1的节点
            // 重新链接到低和高列表中,保持原有的顺序
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;

            // 初始化计数器,统计每组中的节点数量
            int lc = 0, hc = 0;

            // 遍历当前链中的所有节点
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                // 获取下一个节点
                next = (TreeNode<K,V>)e.next;

                // 断开原始链中的下一个节点的链接
                e.next = null;

                // 根据哈希值的特定位判断节点属于哪一组
                if ((e.hash & bit) == 0) {
                    // 将节点添加到低组列表中,保持列表顺序
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;

                    // 增加低组节点计数器
                    ++lc;
                }
                else {
                    // 将节点添加到高组列表中,保持列表顺序
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;

                    // 增加高组节点计数器
                    ++hc;
                }
            }

            // 处理低组节点
            if (loHead != null) {
                // 如果低组节点数量小于等于非树化阈值,则非树化并更新表格
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    // 否则,将低组头部节点设置为表格中的对应位置
                    tab[index] = loHead;

                    // 如果高组头部节点不为空,
                    //说明原本的红黑树要拆
                    //就需要将低位链表重新树化
                    if (hiHead != null) 
                        loHead.treeify(tab);
                }
            }

            // 处理高组节点
            if (hiHead != null) {
                // 如果高组节点数量小于等于非树化阈值,则非树化并更新表格
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    // 否则,将高组头部节点设置为表格中的对应位置
                    tab[index + bit] = hiHead;

                    // 如果低组头部节点不为空,则将高位链表树化
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

 (6)treeifyBin()方法

确定是否需要树化的方法,如果哈希表的大小小于64,则继续扩容。否则树化。先将所有节点转化为树节点,后面由树化方法实现树化。

    /**
     * 将指定bin中的节点转换为红黑树结构,前提是节点数量超过树化阈值。
     * 当尝试将新键放入已包含过多节点的哈希桶时,会调用此方法,
     * 此时它试图将链表结构转换为红黑树以提高查找效率。
     *
     * @param tab 哈希表数组
     * @param hash 插入键的哈希值
     */

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        // 检查哈希表是否为空或当前容量是否小于最小树化容量64
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
            // 如果是,进行扩容操作
            resize();
        } else if ((e = tab[index = (n - 1) & hash]) != null) {
            //否则就开始树化链表
            //1.先将单链表转化为双链表
            //    1.1do-while循环遍历每一个链表节点,先将遍历到的节点转化为树节点
            //    1.2将树节点插入双链表
            //2.再将双链表树化treeify()方法树化


            // 初始化红黑树节点头和尾指针
            TreeNode<K,V> hd = null, tl = null;
            // 遍历链表,将链表节点转换为红黑树节点,并构建红黑树
            do {
                //这里是将链表节点妆化为树节点
                //TreeNode继承LinkHashMap$Entry,这个父又继承HashMap$Node
                //所以还是next这个下一跳的属性
                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);
        }
    }

(7) treeify()方法

 这里树化的代码跟平衡二叉树和红黑树-CSDN博客里面说的一样。

        /**
         * 将从当前节点开始的链表转换为红黑树结构,以提高查找效率。
         * 当链表长度超过一定阈值时调用此方法,确保映射的查找效率。
         *
         * @param tab 表示节点数组的哈希表。
         */

        final void treeify(Node<K,V>[] tab) {
            // 初始化红黑树的根节点
            TreeNode<K,V> root = null;
                
            //外层循环 每层循环取出一个节点
            // 第一层循环的节点作为红黑树的根
            // 其他几层的循环先经过内层循环找到插入的位置插入

            // 遍历从当前节点开始的链表直到末尾
            //for里面的this:因为treeify()方法是HashMap$TreeNode类的,
            //所以this指的是头树节点
            //x: 当前树节点
            //next: 当前树节点的下一节点
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                // 更新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;      // 获取当前遍历节点的键

                        // 根据哈希值确定遍历方向
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        //下面else-if模块尝试通过键对象反射获取键的类,再获取比较器
                        //比较出来后决定进入左边还是右边
                        else if ((kc == null && (kc = comparableClassFor(k)) == null) || 
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            // 如果键相等或没有比较器,则使用tieBreakOrder方法来决定方向
                            dir = tieBreakOrder(k, pk);

                        // 记录当前遍历节点的父节点
                        TreeNode<K,V> xp = p;

                        // 根据方向确定子节点是否为空,如果为空则插入当前节点并进行平衡调整
                        //不为空,就进入比较器得到dir得到的位置,继续内层循环
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;          // 设置父节点
                            if (dir <= 0)
                                xp.left = x;        // 插入到左子树
                            else
                                xp.right = x;       // 插入到右子树

                            // 调整红黑树的平衡性(维护红黑规则)
                            // 可以参考发过的红黑树的笔记,
                            // 里面有张图说如何维护红黑规则
                            // balanceInsertion()方法里面的流程跟图上的差不多
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }

            // 将根节点移动到哈希表的前端
            //里面有两大步骤
            //1.将哈希表对应的位置哈希桶存放的双链表的队头 换成 刚刚树化的红黑树的树根节点
            //2.将在双链表的根节点取出,插入到队头  
            moveRootToFront(tab, root);
        }

(8)untreeify()方法

在resize()方法里面用到,不满足树化,将哈希桶的树节点转化为链表节点

        /**
         * 将树节点转换为链表节点列表。
         * 当不再需要树结构时,此方法用于将树节点转换回链表节点,以保持HashMap的结构。
         * 从当前节点开始遍历所有节点,并使用replacementNode方法替换每个节点为新的链表节点。
         *
         * @param map 节点正在被转换的HashMap。
         * @return 转换后的链表头部节点。
         */

        // 定义一个最终的泛型方法untreeify,接受一个HashMap类型的参数map
        final Node<K,V> untreeify(HashMap<K,V> map) {

            // 初始化链表的头节点和尾节点,初始时都设为null
            Node<K,V> hd = null, tl = null;

            // 遍历从当前节点开始的所有节点
            for (Node<K,V> q = this; q != null; q = q.next) {

                // 使用HashMap的replacementNode方法创建一个新的链表节点p
                Node<K,V> p = map.replacementNode(q, null);

                // 如果是第一个节点,设置它为链表的头节点
                if (tl == null)
                    hd = p;

                // 否则,将当前尾节点tl的next指针指向新节点p
                else
                    tl.next = p;

                // 更新尾节点为新节点p
                tl = p;
            }

            // 返回转换后链表的头节点
            return hd;
        }

(9) balanceInsertion()方法

        /**
         * 在红黑树中插入节点后,通过旋转和重新着色来平衡树。
         * 此方法用于维护红黑树的性质:每个节点要么是红色的,要么是黑色的;
         * 根节点是黑色的;每个叶子节点(NIL节点)是黑色的;如果一个节点是红色的,
         * 则它的子节点必须是黑色的;从任意节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
         *
         * @param root 红黑树的根节点
         * @param x 插入的新节点
         * @return 平衡后的红黑树的根节点
         */
        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;;) {
                // 如果节点x的父节点为空,说明x已经到达树的根节点
                if ((xp = x.parent) == null) {
                    // 将x设置为黑色,结束循环
                    x.red = false;
                    return x;
                }
                // 如果x的父节点xp不是红色的,或者xp的父节点xpp为空,则树已平衡,返回根节点
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                // 确定x位于其父节点xp的左侧还是右侧
                if (xp == (xppl = xpp.left)) {
                    // 如果x的父节点的兄弟节点是红色的
                    if ((xppr = xpp.right) != null && xppr.red) {
                        // 递归调整颜色,将xppr、xp和xpp的颜色翻转,并将x指向xpp,继续循环
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    // 如果x是xp的右子节点,通过左旋转将xp移动到x的位置,然后更新变量,继续循环
                    else if (x == xp.right) {
                        root = rotateLeft(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    // 如果xp是红色的,将其设置为黑色,将xpp设置为红色,并对xpp进行右旋转,然后继续循环
                    if (xp != null) {
                        xp.red = false;
                        if (xpp != null) {
                            xpp.red = true;
                            root = rotateRight(root, xpp);
                        }
                    }
                }
                // 如果x位于其父节点xp的右侧
                else {
                    // 如果xp的兄弟节点xppl是红色的
                    if (xppl != null && xppl.red) {
                        // 递归调整颜色,将xppl、xp和xpp的颜色翻转,并将x指向xpp,继续循环
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    // 如果x是xp的左子节点,通过右旋转将xp移动到x的位置,然后更新变量,继续循环
                    else if (x == xp.left) {
                        root = rotateRight(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    // 如果xp是红色的,将其设置为黑色,将xpp设置为红色,并对xpp进行左旋转,然后继续循环
                    if (xp != null) {
                        xp.red = false;
                        if (xpp != null) {
                            xpp.red = true;
                            root = rotateLeft(root, xpp);
                        }
                    }
                }
            }
        }

(10)moveRootToFront()方法

        /**
         * 确保给定的根节点成为其所在桶中的第一个节点。
         * 此方法用于维护散列表内部结构,确保指定的根节点位于其对应桶的最前端,
         * 这对于优化访问性能或平衡树结构可能是必要的。
         *
         * @param tab 散列表的节点数组,用于存储键值对。
         * @param root 要移动的树节点,同时也是散列表子树的根节点。
         * @param <K> 节点中键的类型。
         * @param <V> 节点中值的类型。
         */

        // 静态泛型方法,用于将树节点移动到其桶的最前面
        //1.将哈希表对应的位置哈希桶存放的双链表的队头 换成 刚刚树化的红黑树的树根节点
        //2.将在双链表的根节点取出,插入到队头  
        static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            // 声明局部变量n
            int n;
            // 检查根节点、表是否非空,并且表至少有一个槽位
            if (root != null && tab != null && (n = tab.length) > 0) {
                // 计算根节点应放置的索引位置
                int index = (n - 1) & root.hash;
                // 获取当前桶中的第一个节点
                TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
                // 当根节点不是桶中的第一个节点时执行以下操作
                if (root != first) {
                    // 将根节点从链表中移除并暂存其后继节点
                    Node<K,V> rn;
                    tab[index] = root;
                    // 获取根节点的前驱节点
                    TreeNode<K,V> rp = root.prev;
                    // 如果根节点有后继节点,则更新后继节点的前驱节点
                    if ((rn = root.next) != null)
                        ((TreeNode<K,V>)rn).prev = rp;
                    // 如果根节点有前驱节点,则更新前驱节点的后继节点
                    if (rp != null)
                        rp.next = rn;
                    // 更新桶中的第一个节点的前驱节点为根节点
                    if (first != null)
                        first.prev = root;
                    // 将根节点插入桶的头部
                    root.next = first;
                    // 设置根节点的前驱节点为空
                    root.prev = null;
                }
                // 断言检查以验证树的不变量
                assert checkInvariants(root);
            }
        }

 

 4.示例

在Set篇说hashSet的时候那两个例子差不多,除了值的覆盖其他都一样。

 java集合之Set篇——HashSet、LinkedHashSet、TreeSet的底层源码解析-CSDN博客

 这里还要说一些Set篇、Map篇没有讲到的——有参构造器创建的集合初始扩容还有树化的过程底层代码。

(1)有参构造器

//参数是一个整数
HashMap<String, String> hashMap = new HashMap<>(7);
hashMap.put("key1", "value1");
//参数是一个hashMap
HashMap<String, String> hashMapS = new HashMap<>(hashMap);

A. HashMap<String, String> hashMap = new HashMap<>(7);

传入初始容量7即参数,还有默认加载因子0.75 

将默认加载因子赋值给集合的加载因子,通过tableSizeFor(初始容量)求集合阈值。 

下面这个方法求新建集合的阈值,简答理解就是cap\leqslant (2^{n})_{min}\left [ n=0,1,2,3... \right ]=threshold(阈值) 最终集合的阈值为8

注意:上面这三步做完哈希表table还是null,添加第一个数据还是要走初始扩容。

B. hashMap.put("key1", "value1")

table为null,走初始扩容,进入resize()方法扩容。 

 初始容量不再是默认容量16,由阈值决定,有了初始容量阈值再重新计算。  

C.HashMap<String, String> hashMapS = new HashMap<>(hashMap);

将默认加载因子赋值给集合的加载因子,通过putMapEntries()方法创建新集合。

 

通过传入的集合的数据个数,计算一个新的数值t,将t传入到tableSizeFor()方法,后面的步骤都跟前面一样。不一样的还有,这里利用内部类HashMap$EntrySet这个内部类,方便遍历传入集合的键和值,在第一次循环的时候,table为null,初始扩容.....跟步骤B一样。

(2)树化过程示例

public class HashMapTypeB {
    public static void main(String[] args){
        HashMap<A, Integer> hashMap = new HashMap<>();
        for (int i = 0; i < 11; i++) {
            hashMap.put(new A("name" + i, i), i);
        }

        System.out.println(hashMap);
    }
}
class A {
    public String name;
    public int age;
    public A(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
    public String getName()
    {
        return name;
    }
    public int getAge()
    {
        return age;
    }

    @Override
    public int hashCode() {//哈希值设置一样,目的插入哈希表同一个位置
        return 100;        //为了更早树化
    }
}

前面的10次循环在Set篇的时候就说过了, 前面8次循环后一条链表上面达到8个数据,第9次循环接第九个数据到单链表后面后判断是否需要树化,因为哈希表还没达到64,先扩容到32。第10次循环的时候,哈希表扩容到64,链表数据10个。

第11次循环就满足树化的两个条件了(链表数据个数>=8 还有  哈希表达到64),下面是树化的底层代码演示过程:

A.从putVal()方法进入判断树化的方法treeifyBin()

B. 进入else if模块,先将单链表的每一个节点循环转化为树节点,最终转化为树节点的双链表。进入到treeify()方法进行树化。

C.进入到treeify()方法,会遍历双链表,每层循环处理一个节点,第一个节点作为根,每个节点插入后要进入balanceInsertion()方法进行维护红黑规则,循环结束后还要进入moveRootToFront()方法,这个方法将在这个双链表中的根节点和队头节点交换,并且将哈希桶的头结点换成根节点。

注意:树化后节点之间还是维护着双链表的,维护红黑规则的方法balanceInsertion()还有刷新哈希桶头结点的方法moveRootToFront()前面的重要方法有详细的注释说明。

 

标签:Map,java,next,链表,源码,哈希,xp,null,节点
From: https://blog.csdn.net/2202_75483664/article/details/140660013

相关文章

  • 自学java第三天
    流程控制语句顺序结构if  else语句。例如:publicclassindex{publicstaticvoidmain(String[]args){intnum1=10;//条件表达式,如果为true执行if后面的语句体,false执行else里面的语句体if(num1>20){//语句体......
  • Java方法
    Java方法System.out.println()System:类out:对象println:方法方法是语句的集合,他们在一起执行一个功能方法是解决一类问题的步骤的有序组合方法包含与类或对象中方法在程序中被创建,在其他地方被引用设计原则本意是功能块,要求保持原子性,即一个方法完成一个功能形式参......
  • 数据结构(Java):HashMap源码解析【JDK17】
    1、整体总结 2、成员属性table哈希数组DEFAULT_INITIAL_CAPACITY哈希表默认容量值(16)MAXIMUM_CAPACITY最大容量值DEFAULT_LOAD_FACTOR默认负载因子threshold当前存放元素数量达到该值时就会触发数组扩容TREEIFY_THRESHOLD树化条件之一(转化为红黑树的阈值)MIN_......
  • 【C++/STL】map和set介绍
    ......
  • 思维导图工具MindMap本地docker一键安装详细教程
    文章目录前言1.Docker一键部署思维导图2.本地访问测试3.Linux安装Cpolar4.配置公网地址5.远程访问思维导图6.固定Cpolar公网地址7.固定地址访问前言本文主要介绍在Linux系统以docker方式一键部署思维导图工具SimpleMindMap,并结合cpolar内网穿透工具实现远程......
  • Linux内核链表源码的简单操作
    一、Linux内核链表源码的获取下载系统源码的方法常见的有两种:第一种访问网站下载:kernel.org第二种输入Linux命令下载:sudoaptinstalllinux-source-5.15.0(一般这种下载的是当前系统所用到的系统源码版本)下载完之后在/usr/src中可找到系统源码的压缩包,可以解压......
  • SpringBoot源码初学者(二):SpringBoot事件监听器
    ps:真正适合阅读源码的新手来看的SpringBoot源码讲解,如果你真的想读懂SpringBoot源码,可以按照以下推荐的方式来阅读文章打开ide,打开SpringBoot源码,跟着文章一起写注释,写自己的注释不要过于纠结没讲到的地方,毕竟SpringBoot源码那么多,想全讲完是不可能的,只要跟着文章认真阅......
  • [Java并发]CountDownLatch
    CountDownLatch概述CountDownLatch一般用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成。有一点要说明的是CountDownLatch初始化后计数器值递减到0的时候,不能再复原的,这一点区别于Semaphore,Semaphore是可以通过release操作恢复信号量的。Co......
  • Java中的ETL工具选型与应用
    Java中的ETL工具选型与应用大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!今天我们将探讨Java中的ETL(Extract,Transform,Load)工具,帮助你了解如何选择和应用这些工具,以便高效地进行数据提取、转换和加载任务。ETL工具在数据工程和大数据处理中扮......
  • 体积计算器(三种语言)源码、效果图
    声明⚠️:英文、藏语翻译均来源网络,若不准确,请于本人联系,请勿抄袭!源码#A-1V=0#体积r=0#小半径R=0#大半径a=0#棱长&长b=0#宽h=0#高DMJ=0#底面积PI=3.14#π(保留两位小数)four_three=1/4*3#四分之三three_one=1/3*1#三分之一R......