Java-01-源码篇-04集合-02-HashMap

目录

一,HashMap

1.1 HashMap 属性分析

1.2 HashMap 构造器

1.3 HashMap 内置的 Node 类

1.4 HashMap 内置的 KeySet 类

1.5 HashMap 内置的 Values 类

1.6 HashMap 内置的 EntrySet 类

1.7 HashMap 内置的 UnsafeHolder 类

1.8 HashMap 相关的迭代器

1.9 HashMap 相关的分割器

1.10 HashMap 内置的 TreeNode (红黑树节点) 类

1.10.1 TreeNode 属性分析

1.10.2 TreeNode 构造器

1.10.3 查看根节点

1.10.4 moveRootToFront

1.10.5 find

1.10.6 getTreeNode

1.10.7 tieBreakOrder

1.10.8 treeify 树结构化处理

1.10.9 untreeify 链表化处理

1.10.10 putTreeVal

1.10.11 removeTreeNode & 红黑树转链表

1.10.12 split

1.10.13 rotateLeft

1.10.14 rotateRight

1.10.15 balanceInsertion

1.10.16 balanceDeletion

1.10.17 checkInvariants

1.11 HashMap 新增业务

1.12 hash处理 

1.13 putVal

1.14 链表转红黑树

1.15 HashMap 删除业务

1.16 HashMap 扩容机制


上一篇Map 接口:Java-01-基础篇 Java集合-01-Map-CSDN博客

一,HashMap

1.1 HashMap 属性分析

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    @java.io.Serial
    private static final long serialVersionUID = 362498820763181265L;
    /** 默认初始容量 */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * HashMap最大容量 
     * 常用于构造函数中指定的最大值。必须是小于等于2的30次方的2的幂(即1073741824)。
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认负载因子:0.75;当容量填充到75%;进行扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 树化阈值;链表将转换成红黑树以提高性能。该值必须大于2,且通常设为8,以与树节点删除时的假设相匹配,即在缩减时会转换回普通链表。
     * 大于8 链表转树
     */
    static final int TREEIFY_THRESHOLD = 8;

    /** 去树化阈值:小于6时,进行树结构转至链表结构; */
    static final int UNTREEIFY_THRESHOLD = 6;

    /** 最小树化容量:64 */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /** HashMap 的主数据,该数组的长度始终是 2 的幂,以确保哈希值分布均匀。*/
    transient Node<K,V>[] table;

    /** entrySet 缓存了 HashMap 中的 entrySet() 视图。这使得在多次调用 entrySet() 方法时不会重复创建新的集合视图。*/
    transient Set<Map.Entry<K,V>> entrySet;

    /** HashMap 数量大小 */
    transient int size;

    /** HashMap 读写次数 */
    transient int modCount;


    /**
     * 扩容阈值:元素数量达到这个值时,哈希表会进行扩容。
     * 默认计算公式:元素数量 * 加载因子(也就是默认情况容量达到75%时触发扩容)
     *     threshold = capacity * loadFactor
     * 如果 table 数组还没有被分配,那么这个字段表示初始数组容量,或者为零。
     * 表示使用默认初始容量(16)
     */        
    int threshold;

    /**
     * 哈希表的负载因子
     */
    final float loadFactor;
    /* 忽略其他代码 */
}

1.2 HashMap 构造器

        设置默认加载因子为:16

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

        创建一个的指定初始化容量大小 HashMap

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

         创建一个指定初始化容量和加载因子大小

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

        将指定的 Map 集合添加到新创建的 HashMap 中;设置加载因子为16;

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

1.3 HashMap 内置的 Node 类

/**
 * 从继承结构可以看出该节点类是支持键值映射的特性
 */
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        
        /**
         * ^ 异或操作在二进制级别对两个数的每一位执行异或运算。即当两位相同时结果为 0,两位不同时结果为 1
         *  键和值的异或运算:
         *      哈希码分布更均匀:
         *      简单且高效:异或操作相对简单且高效,可以快速计算出结果
         *      对称性和混淆:异或操作的对称性和混淆特性使得它适合作为组合哈希码的方法。通过混淆键和值的哈希码,可以有效减少不同键值对生成相同哈希码的概率。
         */
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;

            return o instanceof Map.Entry<?, ?> e
                    && Objects.equals(key, e.getKey())
                    && Objects.equals(value, e.getValue());
        }
    }

        这个Node 就是用来表示HashMap 里面的链表作用,每当HashCode 冲突时,hashMap 底层通过Node 来进行存储形成一个链表。这里的节点都是通过next进行连接。

1.4 HashMap 内置的 KeySet 类

        KeySet 类是 HashMap 的一个重要内部类,通过它可以将 HashMap 的键视为一个集合进行操作。它提供了对 HashMap 键集合的便捷操作,同时确保这些操作会影响底层的 HashMap。

    final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }

        public Object[] toArray() {
            return keysToArray(new Object[size]);
        }

        public <T> T[] toArray(T[] a) {
            return keysToArray(prepareArray(a));
        }

        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (Node<K,V> e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

        KeySet 提供用于迭代 keys 集合,是否包含,提供删除key等操作。

1.5 HashMap 内置的 Values 类

  Values 类是 HashMap 的一个重要内部类,通过它可以将 HashMap 的值集合视为一个集合进行操作。它提供了对 HashMap 值集合的便捷操作,同时确保这些操作会影响底层的 HashMap。

final class Values extends AbstractCollection<V> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<V> iterator()     { return new ValueIterator(); }
        public final boolean contains(Object o) { return containsValue(o); }
        public final Spliterator<V> spliterator() {
            return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
        }

        public Object[] toArray() {
            return valuesToArray(new Object[size]);
        }

        public <T> T[] toArray(T[] a) {
            return valuesToArray(prepareArray(a));
        }

        public final void forEach(Consumer<? super V> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (Node<K,V> e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e.value);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

        提供了值集合的迭代,包含,转换数组等等操作。

1.6 HashMap 内置的 EntrySet 类

        这个类已经很明显了,KeySet 是用于存储键的集合,Values 是用于存储值的集合;EntrySet是用于存储映射实体(键值对)的集合,EntrySet 是一个Set集合。提供删除,是否包含指定元素,迭代元素等操作;代码如下:

    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry<?, ?> e))
                return false;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(key);
            return candidate != null && candidate.equals(e);
        }
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry<?, ?> e) {
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (Node<K,V> e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

1.7 HashMap 内置的 UnsafeHolder 类

        UnsafeHolder 故名意思是Unsafe 的拥有者,Unsafe 类是一个支持直接操作内存和原子性擦操作,并且可以跳过JVM的对象管理机制。这HashMap 中的作用体现仅仅只是在序列化的时候,对负载因子进行存储;代码如下:

    private static final class UnsafeHolder {
        
        private UnsafeHolder() { throw new InternalError(); }
        // 创建一个unfase 实体类
        private static final jdk.internal.misc.Unsafe unsafe
                = jdk.internal.misc.Unsafe.getUnsafe();
        // 获取 loadFactor 负载因子字段属性的相关操作
        private static final long LF_OFFSET
                = unsafe.objectFieldOffset(HashMap.class, "loadFactor");
        static void putLoadFactor(HashMap<?, ?> map, float lf) {
            unsafe.putFloat(map, LF_OFFSET, lf); // 设置负载因子属性
        }
    }

        HashMap 中的序列化

        是的,整个HashMap 代码中 UnsafeHolder 类只有这一处调用了。

1.8 HashMap 相关的迭代器

1.9 HashMap 相关的分割器

1.10 HashMap 内置的 TreeNode (红黑树节点) 类

        这个类就是大名鼎鼎的红黑树结构的节点类。

        当哈希表 table 属性字段中的节点数量超过树化阈值 (TREEIFY_THRESHOLD =8)时,链表结构(Node)将转成一颗红黑树结构(TreeNode)。当转变成红黑树时,其最小的初始容量是64;是原本初始容量(16)的4倍;

        当HashMap 数量小于去树化阈值(UNTREEIFY_THRESHOLD = 6) 时,HashMap 将从一颗红黑树结构(TreeNode)转成链表结构(Node);

1.10.1 TreeNode 属性分析

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        /* 
         * 指向当前节点的父节点,红黑树是一种二叉树结构,
         * parent 属性用于维护节点之间的父子关系,在树的插入,删除和旋转操作中会频繁使用到这个属性
         */
        TreeNode<K,V> parent;  // red-black tree links
        /**
         * 指向当前节点的左子节点,红黑树是有序二叉树
         * 左子节点 < 父节点
         * left属性在于维护左子节点的链接
         */
        TreeNode<K,V> left;
        /**
         * 指向当前节点的右节点,红黑叔是有序二叉树
         * 父节点 < 右子节点
         * right属性在于维护右子节点的链接
         */
        TreeNode<K,V> right;
        /**
         * 当前节点的前置节点;
         *
         */
        TreeNode<K,V> prev;
        /**
         *  标识当前节点的颜色。红黑树的每个节点都有一个颜色属性(红色或黑色),用于维护树的平衡。red 属性为 true 表示节点是红色,为 false 表示节点是黑色
         */
        boolean red;

        /* 忽略其他代码 */
    }

1.10.2 TreeNode 构造器

        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

1.10.3 查看根节点

        /**
         * 循环不断向上查找根节点,根节点的判断依据就是根节点本身的父节点为 null
         */  
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }

1.10.4 moveRootToFront

        指定的红黑树根节点移动到哈希桶的前端。此方法确保在哈希桶中根节点总是位于最前面,以便在后续操作中更容易访问。

        static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            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);
            }
        }

1.10.5 find

         在红黑树中查找特定键值 k。该方法通过比较哈希值和键来遍历树节点,以查找与给定键匹配的节点

        final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
            return null;
        }

1.10.6 getTreeNode

       在红黑树结构中查找特定的键值。

        final TreeNode<K,V> getTreeNode(int h, Object k) {
            return ((parent != null) ? root() : this).find(h, k, null);
        }

1.10.7 tieBreakOrder

        在比较两个对象时进行“平局破坏”(tie-breaking),即当两个对象的比较结果相等或无法直接比较时,使用一些额外的规则来确定它们的顺序

        static int tieBreakOrder(Object a, Object b) {
            int d;
            if (a == null || b == null ||
                (d = a.getClass().getName().
                 compareTo(b.getClass().getName())) == 0)
                d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                     -1 : 1);
            return d;
        }

1.10.8 treeify 树结构化处理

        将链表节点转换为红黑树节点,并将其插入到树结构中

        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;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        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;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

1.10.9 untreeify 链表化处理

        红黑树节点转换为普通链表节点

        final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            for (Node<K,V> q = this; q != null; q = q.next) {
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
        }

1.10.10 putTreeVal

  putTreeVal 方法是 HashMap 的红黑树版本的 putVal 方法,用于将一个新的键值对插入到红黑树中

        final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            TreeNode<K,V> root = (parent != null) ? root() : this;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

1.10.11 removeTreeNode & 红黑树转链表

        removeTreeNode 方法用于从红黑树中删除指定的节点,并在需要时将红黑树转换回链表。removeTreeNode 方法用于从红黑树结构中移除一个节点。它会在 HashMap 的键冲突情况下使用红黑树作为桶内的存储结构时被调用。这个方法主要处理以下步骤:找到要删除的节点调整红黑树的平衡更新链表指针哈希表指针

        /**
         * 从红黑树中删除节点
         * @param map 当前 hashmap 实例
         * @param tab 哈希表 
         * @param movable 是否允许在删除节点后调整树的根节点位置。
         */
        final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
            int n;
            // 1. 检查哈希表是否为空或长度为零。如果是,则直接返回
            if (tab == null || (n = tab.length) == 0) return;

            /*
             * 2. 计算节点所在桶的索引。
             * 初始化 first、root、succ(后置节点)、pred(前置节点)。
             */
            int index = (n - 1) & hash;
            TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
            TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;

            /*
             * 3. 更新链表指针
             */
            // 3.1 更新链表中前置和后置节点的指针。如果前置节点为空,表示当前节点是第一个节点,则更新桶的第一个节点指向后置节点。
            if (pred == null) 
                tab[index] = first = succ;
            else
                pred.next = succ;
            // 3.2 如果后置节点不为空,更新后置节点的前置指针
            if (succ != null)
                succ.prev = pred;
            // 3.3 如果第一个节点为空,直接返回。
            if (first == null)
                return;

            /*
             * 4 处理红黑树根节点
             */
            // 4.1 获取红黑树的根节点
            if (root.parent != null) root = root.root();
            // 4.2 检查根节点是否为空或者是否需要将红黑树转回链表(即当树中节点过少时)。
            if (root == null
                || (movable
                    && (root.right == null
                        || (rl = root.left) == null
                        || rl.left == null))) {
                // 4.3 如果需要转回链表,调用 untreeify 方法。
                tab[index] = first.untreeify(map);  // too small
                return;
            }
            
            /*
             * 5. 查找替代节点
             */
            TreeNode<K,V> p = this, pl = left, pr = right, replacement;
            // 5.1 如果节点有两个子节点,找到后置节点(右子树的最左节点),并交换颜色和位置
            if (pl != null && pr != null) {
                TreeNode<K,V> s = pr, sl;
                while ((sl = s.left) != null) // find successor
                    s = sl;
                boolean c = s.red; s.red = p.red; p.red = c; // swap colors
                // 5.2 更新子节点和父节点的指针
                TreeNode<K,V> sr = s.right;
                TreeNode<K,V> pp = p.parent;
                if (s == pr) { // p was s's direct parent
                    p.parent = s;
                    s.right = p;
                }
                // 5.3 如果节点只有一个子节点或者没有子节点,直接使用子节点作为替代节点。
                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;
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                replacement = p;
             
            /*
             * 6. 更新替代节点指针
             */
            if (replacement != p) { 
                // 6.1 如果替代节点不是当前节点,更新替代节点的父指针
                TreeNode<K,V> pp = replacement.parent = p.parent;
                // 6.2 如果父节点为空,将替代节点设为根节点并设为黑色。
                if (pp == null)
                    (root = replacement).red = false;
                else if (p == pp.left)
                    pp.left = replacement;
                // 6.3 否则,更新父节点的子指针。
                else
                    pp.right = replacement;
                p.left = p.right = p.parent = null;
            }


            /*
             * 7. 平衡红黑树
             */
            // 7.1 如果当前节点是红色,不需要平衡。如果是黑色,调用 balanceDeletion 方法平衡红黑树。
            TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
            // 7.2 如果替代节点是当前节点,断开其与父节点的链接。
            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;
                }
            }
            // 7.3 如果允许移动根节点,将根节点移动到前面
            if (movable)
                moveRootToFront(tab, r);
        }

removeTreeNode 方法从红黑树结构中删除一个节点,主要步骤包括:

  1. 更新链表指针。
  2. 查找替代节点。
  3. 更新指针以移除节点并平衡红黑树。
  4. 根据需要将根节点移动到哈希表桶的前面。

1.10.12 split

        split 方法在 HashMap 的扩容过程中使用,用于将红黑树节点分割成两个链表或树。这个方法将一个红黑树节点数组中的元素重新分配到新的数组中,以便支持哈希表的扩容操作。

        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            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) // (else is already treeified)
                        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);
                }
            }
        }

1.10.13 rotateLeft

        左旋操作的目的是将一个节点(通常是子树的根节点)的右子节点提升为新的根节点,而原来的根节点成为新根节点的左子节点。

        static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
            TreeNode<K,V> r, pp, rl;
            if (p != null && (r = p.right) != null) {
                if ((rl = p.right = r.left) != null)
                    rl.parent = p;
                if ((pp = r.parent = p.parent) == null)
                    (root = r).red = false;
                else if (pp.left == p)
                    pp.left = r;
                else
                    pp.right = r;
                r.left = p;
                p.parent = r;
            }
            return root;
        }

1.10.14 rotateRight

        右旋操作的目的是将一个节点(通常是子树的根节点)的左子节点提升为新的根节点,而原来的根节点成为新根节点的右子节点

        static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
            TreeNode<K,V> l, pp, lr;
            if (p != null && (l = p.left) != null) {
                if ((lr = p.left = l.right) != null)
                    lr.parent = p;
                if ((pp = l.parent = p.parent) == null)
                    (root = l).red = false;
                else if (pp.right == p)
                    pp.right = l;
                else
                    pp.left = l;
                l.right = p;
                p.parent = l;
            }
            return root;
        }

1.10.15 balanceInsertion

        在红黑树中插入节点后调整树以保持其平衡性


        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;
                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);
                            }
                        }
                    }
                }
            }
        }

1.10.16 balanceDeletion

        balanceDeletion 方法在红黑树中删除节点后调整树以保持其平衡性

        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;
                        }
                    }
                }
            }
        }

1.10.17 checkInvariants

        用于递归地检查红黑树的几个不变量,确保树的结构和颜色属性保持一致。这些不变量是红黑树能够提供高效性能的关键。

        static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
            TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
                tb = t.prev, tn = (TreeNode<K,V>)t.next;
            if (tb != null && tb.next != t)
                return false;
            if (tn != null && tn.prev != t)
                return false;
            if (tp != null && t != tp.left && t != tp.right)
                return false;
            if (tl != null && (tl.parent != t || tl.hash > t.hash))
                return false;
            if (tr != null && (tr.parent != t || tr.hash < t.hash))
                return false;
            if (t.red && tl != null && tl.red && tr != null && tr.red)
                return false;
            if (tl != null && !checkInvariants(tl))
                return false;
            if (tr != null && !checkInvariants(tr))
                return false;
            return true;
        }

1.11 HashMap 新增业务

    /**
     * 新增key-value 键值对(映射实体),如果key有旧值,则value覆盖旧值
     * @retrun 返回 key之前所关联的旧值,如果key没有关联值,返回null
     * @param key 要新增的key
     * @param value 要新增的value
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

1.12 hash处理 

        为什么还要对 key进行一次hash 计算,而不是直接调用 key.hashCode() 方法?直接调用key.hashCode() 可能会导致哈希值分布不均匀。要直到HashMap的key 是存储在一条链表上,而具体是链表的哪个位置是通过哈希值来进行计算得到的。如果直接key.hashCode() 这样哈希值容易相同,发生冲突,导致数据分布不均匀;减低存取效率;

        所以通过 hash(key) 多计算一次key的哈希值,使得哈希值不容易相同,减少哈希冲突,数据分布均匀,增加存储效率;

    static final int hash(Object key) {
        int h;
        // key.hashCode() 是获取 key 的哈希码,^ 是按位异或操作
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

        这个 hash 方法通过将原始哈希码的高 16 位与低 16 位进行异或操作,来混合高位和地位的哈希值,这样做的目的是:

        高位扰动:通过将高位和低位的哈希值混合,使得最终的哈希值能够更好地利用哈希表的所有位,从而减少冲突。

        均匀分布:避免因为低位的简单性导致的哈希冲突,使得哈希值在哈希表上分布更均匀。

【案例说明】假设我们有一个简单的键 key1key2,它们的 hashCode 分别是 0x11111111 和 0x11111110。如果不进行高位扰动,这两个键的低位哈希值很接近,容易导致哈希冲突。而经过 hash 方法处理后,这两个哈希值会变得不同,更加均匀地分布在哈希表中。

        所以,hash(key) 方法通过对键的 hashCode 进行进一步处理,确保哈希值分布均匀、减少冲突、提高性能,是 HashMap 能够高效工作的关键之一。这种方法通过混合高位和低位的哈希值,使得哈希值在哈希表中更加均匀地分布,避免了简单哈希算法可能导致的冲突问题。 

1.13 putVal

        此时putVal 方法就是真正新增,代码如下:

    /**
     * 新增key-value 键值对
     * @param hash            key的哈希值
     * @param key             要新增的key
     * @param value           要新增的value
     * @param onlyIfAbsent    如果为 true,则只有当键不存在时才插入 (put方法传入 false)
     * @param evict           是否在插入节点后进行驱逐 (put方法传入 true)
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; /* map集合,里面存储所有键值对 */
        Node<K,V> p; /* p 是键值对应位置的第一个节点,如果该位置有冲突,则 p 是链表或红黑树的根节点 */
        int n, i; /* n: tab map集合的长度,i: 哈希值和数组长度计算得到的索引值 */

        // 1. 初始化或扩容:检查 table 是否为 null 或长度为 0,如果是,则通过调用 resize 方法进行初始化或扩容,并更新 n 为新表的长度
        if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
        
        /*
         * 2. 计算索引: 通过 使用 (n - 1) & hash 计算出键在数组中的索引 i
         * 并检查该索引处是否为空。如果为空,则直接在该位置插入新节点。
         */
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 将新增的需要的数据统一封装成一个节点;
            tab[i] = newNode(hash, key, value, null);
        
        else { // 3. 处理冲突
            Node<K,V> e /* e: element 缩写;*/; K k / k: key 的缩写 表示p的key*/;
            // 3.1 索引处已经有其他节点(发生冲突),检查该节点 p 是否与要插入的键具有相同的哈希值和键。如果相同,则 e = p,表示该键已经存在
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 3.2 如果此时 p的节点类型是 TreeNode 红黑树节点,则调用putTreeVal插入红黑树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 3.3 如果索引处的节点 p 不是 TreeNode,则在链表中插入新节点
            else {
                // 遍历链表,如果找到相同的键,
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        // 将数据封装成链表节点,并追加在p的后置节点,
                        p.next = newNode(hash, key, value, null);
                        // 如果当前map数量大于或等于树化阈值,则进行链表转红黑树
                        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;
                }
            }
            //4 更新现有节点: 如果键已经存在(e != null),则根据 onlyIfAbsent 标志更新节点的值,并返回旧值。
            if (e != null) {// put是传入 false, 意味着key存不存在都进行插入
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount; // 记录修改次数
        if (++size > threshold) // 进行扩容处理
            resize();
        afterNodeInsertion(evict); // 调用 afterNodeInsertion 进行一些插入后的处理
        return null;
    }

        这段代码是 HashMap 插入操作的核心实现,它处理了哈希冲突、链表插入、树化以及扩容等一系列复杂操作,确保 HashMap 能够高效地进行键值对的插入和存储。

1.14 链表转红黑树

    /**
     * 链表转红黑树,当 HashMap 数量大小大小等于树化阈值(64)
     * @param tab 哈希表
     * @param key的hash值
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 进行扩容,当tab == null 或者容量小于 最小树化容量(64)
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
            resize();
        /*
         * index = (n - 1) & hash 计算出 hash 在哈希表中的索引
         * 如果 tab[index] 位置上有节点存在(即不为空),则继续转换链表为红黑树
         */
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            /* hd 红黑树的头部节点,t1 红黑树的尾部节点 */
            TreeNode<K,V> hd = null, tl = null;
            do {
                // replacementTreeNode 转换成红黑树
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null) // 尾部节点为空,意味着这是第一个节点,所以设置给头部节点
                    hd = p;
                else {
                    // tl 不为 null,则将当前节点 p 的 prev 指向 tl,并将 tl 的 next 指向 p
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p; 更新 tl 为当前节点 p。
            } while ((e = e.next) != null); // 循环链表,直到最后一个节点为止
            
            // 将哈希表索引 index 处的链表头节点替换为红黑树的头节点 hd。
            if ((tab[index] = hd) != null)
                hd.treeify(tab); // 如果 hd 不为空,则调用 hd.treeify(tab) 方法,将链表转换为红黑树结构。
        }
    }

    // For treeifyBin
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

总结

  • 检查和扩容:首先检查哈希表是否为空或长度是否小于最小树化容量,若是,则扩容哈希表。
  • 链表转换为红黑树节点:遍历哈希表指定索引位置的链表,将链表中的每个节点转换为红黑树节点。
  • 树化链表:将转换后的红黑树节点替换原链表节点,最后调用 treeify 方法将链表转换为红黑树。

        这个方法的目的是在链表中的节点数量达到一定阈值时,将链表转换为红黑树,以提高哈希表在大量冲突情况下的性能。

1.15 HashMap 删除业务

        remove 方法是 HashMap 用于删除键值对的实现。它通过键删除相应的键值对,并返回被删除的值。

    public V remove(Object key) {
        Node<K,V> e;
        // 调用 removeNode 方法,执行实际的删除操作。
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    /**
     * 删除原始
     * @param hash        key的hash值
     * @param key         要删除的key
     * @param value       key对应的value
     * @param matchValue  是否需要匹配值。如果为 true,则只有键和值都匹配的节点才会被删除。
     * @param movable     是否允许在删除节点后移动其他节点以保持哈希表的结构。
     */
    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 = table:获取哈希表数组。
         * n = tab.length:获取哈希表的长度。
         * (n - 1) & hash:计算键的索引。
         * p = tab[index]:获取对应索引位置的节点。如果哈希表为空或索引位置没有节点,则不进行任何操作。
         */
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            // 1. 查到节点 初始化目标节点 node;
            Node<K,V> node = null, e; K k; V v;
            // 1.1 检查 p 节点是否为要删除的节点(哈希值和键相等)。如果是,设置 node = p。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            /*
             * 1.2 如果 p 不是目标节点,检查 p 是否有后续节点。
             *     如果 p 是 TreeNode 类型,调用 getTreeNode 方法查找红黑树中的节点。
             *     否则,遍历链表查找目标节点。
             */
            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);
                }
            }
            // 2. 删除节点:如果找到了匹配的节点 node,并且值匹配(如果需要匹配值),则进行删除操作。
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                // 2.1 如果 node 是 TreeNode,调用 removeTreeNode 方法从红黑树中删除节点
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                // 2.2 如果 node 是链表的第一个节点,将 tab[index] 指向 node.next。
                else if (node == p)
                    tab[index] = node.next;
                else // 否则,将 p.next 指向 node.next;这样循环指向由 GC进行收回
                    p.next = node.next;
                // 更新修改次数 modCount 和大小 size
                ++modCount;
                --size;
                // 调用 afterNodeRemoval 方法进行后续处理。
                afterNodeRemoval(node);
                return node; // 返回删除的节点。
            }
        }
        return null; // 未找到节点
    }

1.16 HashMap 扩容机制

        HashMap 的扩容业务,当哈希表(table) 为空,或者长度为0,就会触发扩容机制。resize()扩容机制的返回值是扩容后的新表。

       

        源码讲解如下:

    final Node<K,V>[] resize() {
        // 获取旧的哈希表
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        
        // 【第一种情况扩容】:如果旧表容量大于0 进行扩容
        if (oldCap > 0) {
            // 如果旧表容量已经达到最大容量,则无法扩容,直接返回旧表,并且阈值设置为最大值Integer.MAX_VALUE
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 否则,将容量和阈值均扩大为原来的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;
        }
        // 【第二种情况扩容】如果旧表容量为0,但是阈值大于0 ,则将阈值设置为容量的大小
        else if (oldThr > 0) 
            newCap = oldThr;

        // 【第三种情况扩容】如果旧表容量和阈值都为零,则全部都默认初始化容量和扩容阈值
        else {
            newCap = DEFAULT_INITIAL_CAPACITY; // 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 0.75 * 16
        }

        // 【保障处理:确保阈值设置成功】如果新的阈值仍然为 0,重新计算阈值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        // 创建一张新的哈希表
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // 将旧表for循环添加到新的哈希表
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // null 元素直接赋值,e.hash 是用hash进行计算出索引位置
                    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;
                            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;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab; // 返回新的哈希表
    }

  • 35
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值