关于集合相关类

ArrayList

基于动态数组实现的一个容器,具有数组的特性,增删改很快,删除稍微慢。

几个属性

	//第一次扩容时默认的扩容容量
    private static final int DEFAULT_CAPACITY = 10;
	//空的数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
	//默认的空的数组,和上边其实一样的
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	//官方定义为元素缓存,这个是真正存放数据的数组
    transient Object[] elementData; // non-private to simplify nested class access
	//elementData数组中的元素个数
    private int size;

构造函数

//这个是初始时自定义一个初始容量的数组
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

//最常用的构造
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

扩容的关键代码

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

 private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
    //这玩意是list修改的一个计数器,用于迭代器中,防止在迭代器遍历时元素修改
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    //这里是关键的扩容代码,其实就是老数组容量扩大一半
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

注意点
1、数组定义时如果使用 List li = new ArrayList();这种,那么数组其实是{},一个空的,只有当第一个元素添加时,才会扩容;
2、扩容时,这里就用到上面属性中说的:DEFAULTCAPACITY_EMPTY_ELEMENTDATA 了,还有一个属性也表示一个空的数组,就是EMPTY_ELEMENTDATA ,它们的区别在这里就体现出来了:标识是否初始时有没有自定义容量。如果没有,那么第一次扩容就是10咯!
3、关于ArrayList的三个构造,我其实觉得没什么区别,如果在知道要存入的元素个数时,最好手动指定容器大小,这样就避免了不必要的扩容,但是性能优化并不是很明显
4、它不是线程安全的,如果想要线程安全,官方建议是使用:List list = Collections.synchronizedList(new ArrayList(…)); 这种

LinkedList

使用双向链表实现的容器,具有双向链表的特性。LinkedList的底层维护了一个双向链表,它同时实现了List接口和Deque接口,因此它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack),因为它实现了deque接口

属性

transient int size = 0;

    //指向链表的第一个节点的指针
    transient Node<E> first;

    //指向链表的最后一个节点的指针
    transient Node<E> last;

双向链表结构实现

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

增加操作主要代码块

//在链表的最后追加一个元素
public boolean add(E e) {
        linkLast(e);
        return true;
    }

void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

注意点
1、当第一次添加元素前,last和first指针都是null,添加后首指针和尾指针都指向了第一个元素
2、在链表尾添加元素其实就两个动作:将最后一个节点的next指针指向新节点;新节点的pre指针指向最后一个节点

删除操作主要代码块

//删除链表这个位置的元素
public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

//返回这个位置的节点
Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

//将这个节点从链表中删掉,其实就是把这个节点的pre和next指针置空,它的前节点的next指针指向它的后一个节点,它的后一个节点的pre指针指向它的前一个节点
E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

注意点
1、传入的这个index并不是LinkedList的下标,而是从first指针开始的第几个元素。
2、所有的操作都是基于双向链表实现。
3、LinkedList也不是线程安全的,如果想线程安全,官方建议是使用List list = Collections.synchronizedList(new LinkedList(…));

ArrayList和LinkedList的区别

因为ArrayList底层是动态数组,是顺序存储,对于访问操作,可以直接定位到指定位置,但是增加有可能要扩容,需要时间,删除会影响数据下标,需要重排,需要时间,它是随机访问,时间复杂度为O(1)

LinkedList底层是双向链表,是线性存储,对于访问操作,需要指针从前往后遍历查找,不能直接定位。

对于删除和增加操作,只需要找到指定位置添加即可,它是顺序访问,时间复杂度是线性的,随着元素的增多,时间可能会延长

Stack & Queue

Java中的deque接口是Statck和queue的接口。实现了deque,就能实现栈或者队列。
当使用栈或者队列时,可以使用LinkedList或者更高效的ArrayDeque。

Deque是继承Queue接口的接口,它代表了双向队列,即可以在头和尾进行操作 它的两个实现类是ArrayDeque和LinkedList

ArrayDeque

实现了Deque接口,所以,可以实现栈或者队列,同时也是栈或者队列的首选

特性:
1、和ArrayList一样,也是动态数组实现
2、线程不安全
3、比较适合作为双向队列使用

HashMap

1、HashMap类与HashTable非常相似,不同的是HashMap是非线程安全的,并且允许为null。

2、HashMap对于get和put操作的时间复杂度是常数级别的

3、HashMap有两个参数会影响它的性能:initial capacity 和 load factor。

initial capacity 是哈希表中buckets的数量

load factor是一种方法,涉及到自动扩容,负载系数用来指定自动扩容的临界值 ,当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。

当哈希表中的条目超过了负载系数 和当前容量时,哈希表被重新格式化(即内部数据结构)

load factor默认是 .75,它是时间复杂度和空间复杂度的折中,它的值越高,空间复杂度越低,时间复杂度越高。反之亦然。

不能将初始容量(initial capacity)设太高或负载系数 (load factor)设太低

4、与TreeMap相比,它不保证元素的顺序

5、根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java7 HashMap采用的是冲突链表方式。

6、将对象放入到HashMap或HashSet中时,有两个方法需要特别关心: hashCode()和equals()。hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象”。所以,如果要将自定义的对象放入到HashMap或HashSet中,需要@OverridehashCode()和equals()方法。

7、有两个参数可以影响HashMap的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。

冲突链表

根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)

冲突链表中,哈希表的每一个地址存放一个指针指向一个链表,这个链表包含了所有的被哈希为这个地址的值。
key其实就是指针数组的下标,value对应的是这个下标指针指向的链表,这个链表里面存储的就是这个key,也就是下标对应的value值!

冲突链表的优缺点:

优点:

1、当被存储的key value的数量大于哈希表的存储数量时,依然是有效的,可利用的

2、性能优越,有效的算法来处理冲突碰装

缺点:

1、当存储的key的数量不断增加,哈希表的性能会越来越低。换句话说,就是指针数组的存储位置越大,哈希表的性能越低。也就是其他人说的bucket的大小,bucket就是指针数组,entity对应的是链表

例如:冲突链表有1000个内存地址,存储了10000个keys,它比存储位置为10000的冲突链表的性能要小5到6倍

2、遗传了链表的缺点,当存储value时,创建指针开销很大,其次,遍历链表的缓存性能较差,使处理器缓存无效,我也不懂,书上这么说的,大概意思是说,在查找和删除操作中,需要线性遍历链表,直到找到对应的value,随着链表的长度越大,时间复杂度越高(从最坏的情况考虑)。

结论:

冲突链表对于添加操作是O(1),对于查找和删除操作是O(n),n为链表的长度,所以,它适合添加操作,对于迭代比较频繁的场景,不宜将HashMap的初始大小设的过大

java8

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成

根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。

为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)

Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。

我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的

//默认初始容量,必须为2的指数
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  //最大的容量,必须为2的指数且小于2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
//
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;
        }

//链表节点结构
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;
        }

//红黑树节点结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<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;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

map.put过程描述:

1、map.put(”暴躁“,”小刘“);

2、获取”暴躁“字符串的hash值

3、经过hash值扰动函数,使hash值更加散列

4、构造出node对象(hash->1122 key->”暴躁“ value->”小刘“) next->null)

5、路由算法,找出node对应的数组的位置

HashMap源码分析

静态常量

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


    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final float DEFAULT_LOAD_FACTOR = 0.75f;		

//当链表长度超过这个阈值时,且哈希表中存储的节点超过64,链表转为红黑树
    static final int TREEIFY_THRESHOLD = 8;  树型阈值

     //当红黑树的节点个数小于6的时候,变为链表
    static final int UNTREEIFY_THRESHOLD = 6;   非树型阈值

     //当哈希表中的节点个数超过64时,才能有机会将链表转为红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;

总结几点:容量必须为2的倍数,且最大容量不超过2的30次方

默认的负载因子是0.75

字段

 transient Node<K,V>[] table;  哈希表

    transient Set<Map.Entry<K,V>> entrySet;  

    transient int size;  当前哈希表中元素个数

    transient int modCount;  结构修改计数器

    int threshold;  扩容阈值,当你的哈希表中的元素超过阈值时,触发扩容  翻译为:门槛		

    final float loadFactor;  负载因子

总结几点:负载因子就是一个常量0.75,这个值是最有效的,我们不用去改

threshold = capacity * 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); 
    }

//下边这两个静态方法用于将cap转为2的倍数
 static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

@IntrinsicCandidate
    public static int numberOfLeadingZeros(int i) {
        // HD, Count leading 0's
        if (i <= 0)
            return i == 0 ? 32 : 0;
        int n = 31;
        if (i >= 1 << 16) { n -= 16; i >>>= 16; }
        if (i >= 1 <<  8) { n -=  8; i >>>=  8; }
        if (i >= 1 <<  4) { n -=  4; i >>>=  4; }
        if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
        return n - (i >>> 1);
    }

为啥 threshold 不直接等于 initialCapacity * loadFactor呢?

因为哈希表的容量必须是2的倍数,你传进来的initialCapacity通过了校验,但是不能确定是2的倍数,所以,需要tableSizeFor方法返回一个大于等于当前initialCapacity的数字,且这个数字是2的倍数!

put方法

public V put(K key, V value) {
    //这个hash函数就是路由寻址,找到key对应的散列表的下标
        return putVal(hash(key), key, value, false, true);
    }
    
    
//tab:引用当前的hashmap的散列表
//p:表示当前散列表的元素
//n:表示散列表数组长度
//i:表示路由寻址 结果
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    //延迟初始化逻辑,第一次调用putval时会初始化hashmap中的最耗费内存的散列表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    //最简单的一种情况:寻址找到的桶位,刚好是null,这个时候,直接将当前的k-v=>node扔进去即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //e:不为null的话,找到了一个与当前要插入的k-v一致的元素
            //k:表示临时的key
            Node<K,V> e; K k;
            
            //表示桶位中的该元素,与你当前插入的元素的key一致,后续需要替换
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //是红黑树时
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //链表时,而且链表的头元素与我们要插入的key不一致
            else {
                for (int binCount = 0; ; ++binCount) {
                    //表示遍历到了链表最后也没找到与你要插入的key一致的元素,需要加到链表后面
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //说明当前链表的长度,达到了树化标准,需要树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //条件成立的话,说明找到了相同key的node元素,需要进行替换操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //条件成立说明找到了一个与你插入元素key完全一致的数据,需要替换
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
    //插入新元素,size自增,如果自增后的值大于扩容阈值,则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize扩容机制

final Node<K,V>[] resize() {
    //oldTab:引用扩容前的哈希表
        Node<K,V>[] oldTab = table;
    //oldCap:表示扩容前table数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr:表示扩容前的扩容阈值
        int oldThr = threshold;
    //newCap:扩容后的数组大小
    //newThr:扩容后触发扩容的阈值
        int newCap, newThr = 0;
    //条件成立的话,表示hashmap中的散列表已经初始化过,是正常扩容
        if (oldCap > 0) {
            //表示扩容之前的table数组大小已经达到了最大阈值,不能扩容,直接返回即可
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //oldCap左移一位,翻倍赋值给newCap,扩容阈值也翻一倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
    //条件成立,表示oldCap为0,说明hashmap中的散列表是null
    //1、new HashMap(initCap,loadFactor);
    //2、new HashMap(initCap);
    //3、new HashMap(map);  且这个map有数组
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr; 
    //条件成立,表示oldCap为0且oldThr为0
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;  //16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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];
        table = newTab;
    //说明hashmap扩容前table不为null,即有数据
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //说明当前桶位中有数据,但是数据类型不知道,可能是链表或树
                if ((e = oldTab[j]) != null) {
                    //方便jvm  GC垃圾回收
                    oldTab[j] = null;
                    //第一种情况:如果这个节点不存在下一个节点,即不存在哈希碰撞,直接使用路由算法hash&(length-1)得到新哈希表中的桶下标进行存储
                    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;
    }

get方法

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(key)) == null ? null : e.value;
    }

//tab:引用当前hashmap的散列表
//first:桶位中的头元素
//e:临时弄得元素
//n:table数组长度
    final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            //第一种情况:定位出来的桶位元素即为我们要的数据
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //第二种情况:当前桶位不止一个,可能是链表,可能是红黑树
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //第三种情况:桶位形成链表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值