JDK1.8HashMap源码分析说明

目录

1.HashMap数据存储结构

​2.HashMap成员变量

3.HashMap构造方法

4.HashMap数据操作

4.1put(key,value)

4.2get(key)

5.get()和put()方法的辅助操作

5.1resize()

5.2treeifyBin()

6.get()和put()方法的辅助类

6.1Node

6.2KeySet

6.3Values

6.4EntrySet

6.5KeyIterator,ValueIterator,EntryIterator

6.6HashIterator

6.7KeySpliterator,ValueSpliterator,EntrySpliterator

6.8HashMapSpliterator

6.9TreeNode

7.总结


HashMap容量大小为什么是2倍数,扩容也是2倍数扩容?

a.tab[(n - 1) & hash]通过(n-1)&hash计算数组中存放的位置,在tab数组中存放数据时,尽量保证数据能够均匀存放在数组上,降低Hash冲突,便面形成链表结构,这样查询数据也更快速;

例如:初始容量是为16,(n-1)二进制位01111&hash,可以控制和hash的&运算时可以获取0-15的索引数组位置 (n-1后面全是1),保证可以计算出全部的索引位置;

b.在扩容时,newCap = oldCap << 1,newThr = oldThr << 1,数量*2;

oldCap:table数组大小;oldThr:数据容量阀值;

if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; //两倍旧的数据容量阀值

例如:例如扩容以后16变为32,(n-1)二进制位011111&hash,可以控制和hash的&运算时可以获取0-31的索引数组位置 (n-1后面全是1),保证可以计算出全部的索引位置;

c.扩容迁移时,仅有一半的数据要迁移,减少迁移成本;

1.HashMap数据存储结构

在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

下图中代表jdk1.8之前的hashmap结构,左边部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。(此图借用网上的图)

jdk1.8之前的hashmap都采用上图的结构,都是基于一个数组和多个单链表,hash值冲突的时候,就将对应节点以链表的形式存储。如果在一个链表中查找其中一个节点时,将会花费O(n)的查找时间,会有很大的性能损失。到了jdk1.8,当同一个hash值的节点数不小于8时,不再采用单链表形式存储,而是采用红黑树,如下图所示(此图是借用的图)


2.HashMap成员变量

transient:其实这个关键字的作用很好理解,就是简单的一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。

Fail-Fast 机制
我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:注意到 modCount 声明为 volatile(但在JDK7和JDK8中,已经没有这样声明了),保证线程之间修改的可见性。

ConcurrentModificationException:请注意,此异常并不总是表示对象已被其他线程同时修改。如果单个线程发出一系列违反对象约定的方法调用,则该对象可能会抛出此异常。 例如,如果线程使用有fail-fast机制的迭代器在集合上迭代时修改了集合,迭代器将抛出此异常。

    //HashMap存放key,value的节点数组,当被使用时初始化并指定大小,长度总是2的幂(倍数)
    transient Node<K,V>[] table;

    /**
     * 存放entrySet()创建AbstractSet<Map.Entry<K,V>>对象, entrySet字段值在AbstractMap
     * keySet()和values()也会使用,防止重复创建
     */
    transient Set<Map.Entry<K,V>> entrySet; //包含Map.Entry<K,V>的Set集合

    /**
     * Map集合大小
     */
    transient int size;

    //modCount存放HashMap被修改次数,修改包含HashMap中元素数量的变化或者内部结构的变化(例如:rehash)
    //该字段用于使HashMap的集合视图上的迭代器fail-fast机制。(ConcurrentModificationException)。
    transient int modCount;

    //阀值,当达到阀值时,HashMap进行扩容,值大小为(capacity * load factor)
    int threshold;


    //哈希表的负载因子,指达到容量的百分比以后,进行扩容
    final float loadFactor;

默认值

 /**
     * 默认的初始容量 - 必须时2的倍数
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /*最大容量,如果隐式指定更高的值时使用由任何一个带参数的构造函数。
      *必须是2的幂<= 1<<30
    * /
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 加载因子,默认0.75,最大容量比例
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //如果链表超过了这个值,则将单链表转变为红黑树
    static final int TREEIFY_THRESHOLD = 8;

    //如果红黑树的节点被删,且小于该阈值,则变为链表
    static final int UNTREEIFY_THRESHOLD = 6;

    //桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;

3.HashMap构造方法

    //构建一个空的HashMap,并指定容量和负载因子
    //容量会计算指定值最近的2倍数的整数
    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);
    }
    //构建一个空的HashMap,并指定容量,负载因子默认0.75
    //容量会计算指定值最近的2倍数的整数
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //构建一个空的HashMap,容量为16,负载因子默认0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 所有字段默认的
    }

    //将Map放入新的HashMap,容量为16,负载因子默认0.75
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

4.HashMap数据操作

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

从Hash值计算可以看到key为null时,索引位置计算出来为0,存在数组第一个位置;

这里通过位运算重新计算了hash值的值。为什么要重新计算?

主要是因为n值比较小,hash只参与了低位运算,高位运算没有用上。这就增大了hash值的碰撞概率。而通过这种位运算的计算方式,使得高位运算参与其中,减小了hash的碰撞概率,使hash值尽可能散开。

4.1put(key,value)

 /**
     * 存放key和value到map中,如果已经存在key,则value替换新的值
     * 
     *
     * @param key key 和value关联的key
     * @param value 和key关联的value
     * @return 返回value
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * 实现Map.put和关联的方法
     *
     * @param hash key的hash值
     * @param key 键key
     * @param value 存放的值
     * @param onlyIfAbsent 如果为true, 不改变存在的值
     * @param evict 如果为false, table是创建模式,evict参数用于LinkedHashMap中的尾部操作,这里没有实际意义。
     * @return 返回value
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //1.若table为null,则初始化table数组,存放key,value的table在使用时创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //2.[(n - 1) & hash]计算索引位置元素是否为null,为null,则新建Node节点,存放数组指定位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //3.判断是否是数组第一个位置的元素,hash和key相等,替换新的value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //4.判断数组位置第一个元素p是否是Tree树节点,然后添加到红黑树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //5.循环判断数组后面的元素next是否为空
                for (int binCount = 0; ; ++binCount) {
                    //5.1next为null,则新建节点添加到列表后面
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //5.2链表深度>=8(TREEIFY_THRESHOLD),则转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //5.3当前key已存在,则替换新的value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //5.已存在的key,真正替换value的地方,内部判断是否替换
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //6.HashMap修改的次数
        ++modCount;
        //7.HashMap判断容量是否达到阀值,达到则重新创建HashMap中table
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

a.检测table数组是否已创建;

b.添加元素到table数组中,若索引位置为空,则直接将元素放到索引位置;

c.若添加索引位置为红黑树则添加元素到红黑树中;

c.添加以后检测链表是否超过8,则转为红黑树;

d.判断是否需要替换value;

4.2get(key)

 /**
     * 返回指定key对应的value,没有则返回null
     *
     *只能有一个key为null的情况
     *可以通过containsKey判断是否有null做为key,区分没有key或者null为key
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * 实现Map.get和相关方法
     *
     * @param hash hash值做为key
     * @param key 键key
     * @return 返回key对应的节点
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //1.判断table不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //2.总是先检查first节点的key是否等于要查询的key
            if (first.hash == hash && 
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //3.判断下一个节点key
            if ((e = first.next) != null) {
                //3.1判断是否是树Tree节点,是,则调用树的方法查找节点
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //3.2循环判断first链表后面的元素是否存在key
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

5.get()和put()方法的辅助操作

辅助方法包括

resize():新建table数组,或者HashMap扩容;

treeifyBin():将链表转为红黑树;

5.1resize()

/**
     * 初始化或者扩容table的容量  
     * @return the table
     */
    final Node<K,V>[] resize() {
        //1.记录旧的table数组
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //2.判断旧的table数组是否为空
        if (oldCap > 0) {
            //2.1若超过最大容量直接返回
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //2.2容量*2,扩大新table数组存储的容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; //两倍旧的数据容量阀值
        }//3.table为空,已经设置容量,则用旧的容量大小
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;  //数组大小等于旧的阀值
        else {//4.第一次初始化table,则用默认容量值,和阀值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }//5.阀值==0,计算容量*负载因子
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //6.创建table数组,用新的容量值
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //7.扩容时需要把已经存在的数据赋值到新的table数组上
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //7.1循环判断将旧值赋值到新的table上
                if ((e = oldTab[j]) != null) {
                    //旧值赋空
                    oldTab[j] = null;
                    //7.1.1.next节点为空,存放到新的table数组上,索引位置为e.hash & (newCap - 1)
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //7.1.2树节点,需要拆分到新的table数组上
                    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;
                        //7.1.3将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
                        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;
    }

5.2treeifyBin()

    //转换链表长度>=8的位置变成红黑树,若tab比较小,则调用resize()方法
    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)
            resize();
        //找到hash位置链表第一个元素,转换为红黑树
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            //转为双向链表
            do {
                //将Node节点转为TreeNode节点
                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);
        }
    }
    TreeNode
       /**
         * Forms 将双向链表TreeNode节点转换为红黑树
         * @return 跟节点
         */
        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;
                        //hash值相等
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);//打破僵局,判断dir为-1或者1
                        //保存p的临时节点
                        TreeNode<K,V> xp = p; 
                        //-1加到左侧,1为右侧节点,
                        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);
        }
        //对红黑树进行标记,检查是否符合红黑树规则,不符合则进行旋转处理,同时返回跟节点
        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);
                            }
                        }
                    }
                }
            }
        }

 

6.get()和put()方法的辅助类

6.1Node

Node类用户存放Map的键值对;红黑树节点TreeNode类继承自Node类;

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

6.2KeySet

用于提供Map中key的相关操作,获取Map大小,key迭代器,移除等相关操作;

 final class KeySet extends AbstractSet<K> {}

6.3Values

用于提供Map中value的相关操作,获取Map大小,value迭代器等相关操作;

final class Values extends AbstractCollection<V> {}

6.4EntrySet

用于提供Map中Node的相关操作,获取Map大小,Node迭代器,移除等相关操作;

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {}

6.5KeyIterator,ValueIterator,EntryIterator

a.迭代器统一实现HashIterator类;

b.在EntrySet,Values,KeySet中迭代器统一调用的***Iterator相应迭代器;

c.实现取下一个元素时,调用父类HashIterator的nextNode()方法实现获取next元素,第一步先获取链表next元素,没有,则循环table数组下一个元素,返回的是Node节点,迭代返回Node节点或者key和value;

abstract class HashIterator 
final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

6.6HashIterator

实现HashMap迭代器;其他迭代器继承HashIterator,实现定制需求;

int expectedModCount;  // for fast-fail

fast-fail机制,判断 modCount 跟 expectedModCount 是否相等,不相等说明Map迭代过程被其他线程修改或者单个线程发出一系列违反对象约定的方法调用,则抛出异常;

 abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
}

6.7KeySpliterator,ValueSpliterator,EntrySpliterator

KeySpliterator,ValueSpliterator,EntrySpliterator都实现HashMapSpliterator类和Spliterator接口;

Spliterator:Spliterator是一个可分割迭代器(splitable iterator),可以和iterator顺序遍历迭代器一起看。jdk1.8发布后,对于并行处理的能力大大增强,Spliterator就是为了并行遍历元素而设计的一个迭代器,jdk1.8中的集合框架中的数据结构都默认实现了Spliterator;

Spliterator基础讲解可以参考:https://blog.csdn.net/lh513828570/article/details/56673804/

HashMapSpliterator:提供每个迭代器需要的基础信息;

static final class KeySpliterator<K,V>
        extends HashMapSpliterator<K,V>
        implements Spliterator<K> {}

6.8HashMapSpliterator

HashMapSpliterator:提供可分每个迭代器需要的基础信息;

static class HashMapSpliterator<K,V> {
        //用于存放HashMap对象
        final HashMap<K,V> map;
        Node<K,V> current;          // current node
        //起始位置(包含),advance/split操作时会修改
        int index;                  // current index, modified on advance/split
        //结束位置(不包含),-1 表示到最后一个元素
        int fence;                  // one past last index
        //HashMap的size大小
        int est;                    // size estimate
        //用于存放HashMap的modCount
        int expectedModCount;       // for comodification checks
}

6.9TreeNode

TreeNode:红黑树节点,最终继承自Node;

提供红黑树相关操作,链表转红黑树,添加元素,删除元素等;

static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
}

7.总结

1.可以看到HashMap存储数据结构用Node类和TreeNode类;

2.HashMap用到了数组,链表,红黑树,Set集合,迭代球,可拆分迭代器等;

3.HashMap主要包含几部分存数据,取数据,扩容,红黑树处理;

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值