HashMap源码解析

HashMap是Java中高效且线程不安全的Map实现。在JDK1.7中,它使用数组+链表的方式存储数据,而从JDK1.8开始引入了红黑树以优化长链表的查找性能。HashMap的特点包括快速查找,通过Hash算法将key映射到数组下标,并使用链表或红黑树解决冲突。在多线程环境下,JDK1.7的头插法可能导致死循环,而JDK1.8已对此进行了优化。
摘要由CSDN通过智能技术生成

简介:

​ HashMap作为Map的主要实现类;线程不安全的,但是效率高。可以存储null的key和value

底层存储方式

​ HashMap底层原理在JDk1.7以前和JDK1.8以后有着较大的区别;在JDK1.7之前使用的是数组+链表的存储方式,而在JDK1.8以后采用的是数组+链表+红黑树的存储方式。

对HashMap更加深入的了解

​ 为了更加深入的了解HashMap,我们需要了解到其他存储方式的优缺点与HashMap存储方式的优优点;

引入HashMap

数组在定义的时候,需要指定明确的初始容量。它在堆中本质上是一块连续的内存,所以我们可以通过下标快速的地位。

​ 但是一旦数组被定义后,想要安全的对数组进行扩容,就需要重新定义一个更大的数组,将原数组的元素拷贝进去。这样对我们的操作是非常不方便的,所以就需要用到Java集合类了。

ArrayList底层使用了数组,封装了对数组的各种操作,使我们可以更加轻松的操作数据,但是它虽然查询速度快,但进行插入、删除的时候速度就很慢了,这个时候就需要使用LinkedList了。

LinkedList底层使用了链表,对于插入、删除等操作的速度非常快,但是对于查询的速度就很慢。

​ 对于ArrayList 和 LinkedList,还有 Vector它们都有一些缺点,要么插入删除速度慢、要么就是遍历速度慢。那么有没有一种插入、删除、遍历速度都比较不错的集合类呢?于是 HashMap 就出现了。

HashMap的特点
  1. 为了实现快速查找,HashMap 选择了数组而不是链表。以利用数组的索引实现 O(1) 复杂度的查找效率。
  2. 为了利用索引查找,HashMap 引入 Hash 算法, 将 key 映射成数组下标: key -> Index。
  3. 引入 Hash 算法又导致了 Hash 冲突。为了解决 Hash 冲突,HashMap 采用链地址法,在冲突位置转为使用链表存储。
  4. 链表存储过多的节点又导致了在链表上节点的查找性能的恶化。为了优化查找性能,HashMap 在链表长度超过 8 之后转而将链表转变成红黑树,以将 O(n) 复杂度的查找效率提升至 O(log n)。

HashMap的结构图

JDK1.7

在这里插入图片描述

JDK1.8

在这里插入图片描述

JDK1.7的HashMap源码解析

成员变量/类变量

	// 数组默认初始容量为16(必须为2的幂次)
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

	// 数组最大容量为1 << 30(约为1亿)
    static final int MAXIMUM_CAPACITY = 1 << 30;

	// 默认负载因子为0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

	// 当table(Entry数组)未实例化时为空的
    static final Entry<?,?>[] EMPTY_TABLE = {};

	// table为空数组,这个和上面的可以合并在一起
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    // HashMap中集合大小
    transient int size;

	// 阈值,在数组中超过这个数时进行扩容
    int threshold;

	// 负载因子
    final float loadFactor;

	// 修改次数,在添加或删除时+1
    transient int modCount;

Entry类

class Entry<K, V> implements MyMap.Entry<K, V> {

    // key
    private K key;

    // value
    private V value;

    // 指向下一个Entry对象
    private Entry<K, V> next;

    // 存放每个Entry对象的hash值
    private int hash;

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

    public Entry(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public K getKey() {
        return key;
    }

    @Override
    public V getValue() {
        return value;
    }

    @Override
    public V setValue(V value) {
        return this.value = value;
    }

    @Override
    public String toString() {
        return "Entry{" +
            "key=" + key +
            ", value=" + value +
            '}';
    }
}

构造函数

**HashMap()**函数

// 在使用无参构造函数创建HashMap时调用其有参构造函数
public HashMap() {
    // 默认初始化容量为16 	默认加载因子为0.75
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

**HashMap(int initialCapacity, float loadFactor)**函数

public HashMap(int initialCapacity, float loadFactor) {
    // 如果容器初始化容量小于0,则抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 如果初始容量 > MAXIMUM_CAPACITY(越1亿),令初始容量值=最大容器容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 如果加载因子 <= 0 抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    // 给容器加载因子赋值
    this.loadFactor = loadFact0or;
   	// 给容器阈值赋值为初始容量大小
    threshold = initialCapacity;
    // 只是定义一个函数,可以被继承类重载这个方法
    init();
}

HashMap初始化完成,此时HashMap并没有初始化底层数组,只有当第一次添加元素时才会初始化底层数组。

向HanshMap中添加key,value

**put(K key, V value)**函数

public V put(K key, V value) {
    // 如果table为空的话,初始化数组
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果key为空的化,调用putForNullKey(value)将其添加到table[0]上,如果table[0]不为空,则添加它的下一个节点上。
    if (key == null)
        return putForNullKey(value);
    // 计算key的hash值
    int hash = hash(key);
    // 根据hash值和数组长度计算它在数组中的下标
    int i = indexFor(hash, table.length);
    // 遍历下标为i的链表,直到e为空
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 判断hash值与key值是否与链表中的相等,相等的化就覆盖value值
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    // 修改次数+1
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

**inflateTable(int toSize)**函数

private void inflateTable(int toSize) {
    // 获取数组容量大小为16
    int capacity = roundUpToPowerOf2(toSize);
	// HashMap阈值为 capacity * loadFactor = 12.
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 初始化数组,长度为16
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

**addEntry(int hash, K key, V value, int bucketIndex)**函数

向容器中添加key-value键值对

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果满足以下条件,则对数组进行扩容,并且重新计算key在数组中的下标,将其重新存入数组中
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 一般扩容为原数组长度的两倍
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

**resize(int newCapacity)**函数

对HashMap集合进行扩容

void resize(int newCapacity) {
    // 获取旧的数组
    Entry[] oldTable = table;
    // 获取旧的容量
    int oldCapacity = oldTable.length;
    // 如果旧的容量等于最大容量(约1亿),则阈值等于Integer的最大值
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    // 创建容量为新容量的数组
    Entry[] newTable = new Entry[newCapacity];
    // 移动所有数据从旧数组到新数组中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 令HashMap中的table等于newTable。
    table = newTable;
    // 阈值为newCapacity * loadFactor
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

**createEntry(int hash, K key, V value, int bucketIndex)**函数

使用头插法,每次在链表中新插入的节点都在表头。

void createEntry(int hash, K key, V value, int bucketIndex) {
    // 这一段的用法是头插法,将新插入的节点插入到原本的节点的前面。
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // hashMap大小+1
    size++;
}

通过key获取HashMap中的value

**V get(Object key)**函数

public V get(Object key) {
    // 如果key为空,则去table[0]的链表中查找
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    // 如果entry为空,则返回空,否则返回entry.getValue()
    return null == entry ? null : entry.getValue();
}

getEntry(Object key) 函数

final Entry<K,V> getEntry(Object key) {
    // 如果集合中大小为0,则直接返回空
    if (size == 0) {
        return null;
    }

    // 计算key的hash值
    int hash = (key == null) ? 0 : hash(key);
    // 使用key的hash值计算数组下标,并在对应的链表中查询
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        // 满足以下条件则返回value,否则继续遍历链表
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

JDK1.7中存在的问题

在JDK1.7版本使用 链表头插赋值法,在多线程的情况下会导致一个死循环问题。在JDK1.8的时候已经解决该问题

主要看这个函数:

**transfer(Entry[] newTable, boolean rehash)**函数

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        // 增强for循环遍历数组
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            // 令e.next = 新数组中下标为i的链表,如果为空,则下标不冲突。
            e.next = newTable[i];
            // 使用头插法插入链表中,所以原来的链表会反过来
            newTable[i] = e;
            // 使用next遍历链表
            e = next;
        }
    }
}
B.next = A;
A.next = B;
// 产生了闭环

原理分析:因为每次数组在扩容的时候,新的数组长度发生了变化,需要从新去计算index值,需要将原来的table中的数据移动到新的table中;在HashMap1.7版本中的598行代码e.next = newTable[i];直接改变原来的table的next关系。

如果在多线程中,T1线程修改了链表中的结构,而T2线程同时对这个链表进行操作时,就会发生很大的问题。

根本问题是在复制数据的过程中,修改了原本链表的结构,使原本的链表首位相连导致出现脏读的数据。

解决办法:使用ConcurrentHashMap代替

误区:扩容的时候不是重新计算hash值,而是重新计算index,hash值保存在entry中。

JDK1.8源码解析

​ 在了解JDK1.8之前,推荐先去学习红黑树。

成员变量/类变量

	// 默认的初始化容量为16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

	// 最大的容量(约为1亿)
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认的负载因子为0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

	// 转红黑树的阈值
    static final int TREEIFY_THRESHOLD = 8;

	// 桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
    static final int UNTREEIFY_THRESHOLD = 6;

	//最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)
    static final int MIN_TREEIFY_CAPACITY = 64;

	// table数组,用来存储key-value键值对的数组
    transient Node<K,V>[] table;

	// 集合大小
    transient int size;

	// 修改次数
    transient int modCount;

	// 数组扩容的阈值
    int threshold;

	// 负载因子
    final float loadFactor;

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

    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;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

构造函数

无参构造函数

1、HashMap函数

public HashMap() {
    // 负载因子为0.75
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
有参构造函数

**1、HashMap(Map<? extends K, ? extends V> m)**函数

public HashMap(Map<? extends K, ? extends V> m) {
    // 加载因子为0.75
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // 将map集合中的数据存入到新创建的hashMap对象中
    putMapEntries(m, false);
}

**putMapEntries(Map<? extends K, ? extends V> m, boolean evict)**函数

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    // s = 集合map的大小
    int s = m.size();
    // 如果m中存储的元素不为0
    if (s > 0) {
        // 判断table是否已经初始化,如果没有的话则对threshold初始化
        if (table == null) { // pre-size
            // 根据需要阈值计算要创建的HashMap的容量
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                // 把要创建的HashMap的容量存储在threshold中
                threshold = tableSizeFor(t);
        }
        // 如果table不为空,说明已经被初始化,
        // 判断需要插入的数据大小是否大于threshold,如果是的话,就扩容。
        else if (s > threshold)
            resize();
        // 遍历m集合
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            // 获取key、value
            K key = e.getKey();
            V value = e.getValue();
            // 调用putVal函数将其插入map中
            putVal(hash(key), key, value, false, evict);
        }
    }
}

向集合中添加元素

**put(K key, V value)**函数

public V put(K key, V value) {
    // 调用putVal函数进行添加操作,此时底层数组还未创建
    return putVal(hash(key), key, value, false, true);
}

**putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)**函数

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab就是数组中存放的所有的链表
    Node<K,V>[] tab; 
    // p单个链表的某一个节点
    Node<K,V> p; 
    // n就是当前数组的长度
    int n, i;
    // 如果当前我们的hashMap中的table中的数组为空的情况下,就需要进行扩容。(数组初始化)
    if ((tab = table) == null || (n = tab.length) == 0)
        // tab = table,此时tab指向table的地址
        // n为扩容之后tab的长度。
        n = (tab = resize()).length;
    // i就是当前key计算hash值存放在数组中的下标
    // 如果p为空,说明可以将key-value直接存入数组中
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //否则key可能产生了下标冲突
    else {
        Node<K,V> e; K k;
        // 如果p的hash相等,key也相等,则对key的value进行覆盖
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 令e指向p的地址
            e = p;
        // 否则如果p的类型为红黑树节点,如果是红黑树节点,就以红黑树的方式存放
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // binCount 作用可以进行循环
            for (int binCount = 0; ; ++binCount) {
                // 如果没有下一个节点
                if ((e = p.next) == null) {
                    // 则链表的下一个节点为当前key-value 尾插法
                    p.next = newNode(hash, key, value, null);
                    // 如果binCount达到阈值,则转为红黑树存储数据
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果hash值相等,key也相等情况下,实现对我们value的覆盖
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // p指向下一个节点
                p = e;
            }
        }
        // 判断当前e是否为空
        if (e != null) { // existing mapping for key
            // 否则对其value进行覆盖
            V oldValue = e.value;
            // onlyIfAbsent:覆盖,如果为true,则不更改现有值,默认为false:更改现有值。
            // 如果原值为空,则修改value
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 空的,给子类实现的方法
            afterNodeAccess(e);
            // 返回旧的数据
            return oldValue;
        }
    }
    // 修改次数+1
    ++modCount;
    // 如果size > threshold,则对数组进行扩容
    if (++size > threshold)
        resize();
    // 空的,给子类实现的方法
    afterNodeInsertion(evict);
    return null;
}

**resize()**函数

对数组进行扩容操作(resize函数也包含对table的初始化操作)

final Node<K,V>[] resize() {
    // 获取原来table的数组
    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) {
            // 阈值容量 = Integer的最大值
            threshold = Integer.MAX_VALUE;
            // 返回旧的tab
            return oldTab;
        }
        // 新的容量 = 旧的容量 * 2
        // 并且新的容量 >= 默认的容量(16)且小于最大容量限制
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新的阈值容量 = 原来的阈值 * 2
            newThr = oldThr << 1; // double threshold
    }
    // 否则如果旧的容量 = 0,旧的阈值 > 0
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 新的容量 = 旧的阈值
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 新的容量为16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 新的阈值为 16 * 0.75 这是对数组进行初始化
        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);
    }
    // 对当前对象的变量进行赋值
    threshold = newThr;
    // 新的table容量 = 16
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 给table进行赋值
    table = newTab;
	// 如果旧的tab不为空,则将原数组中的数据复制到newTab中
-------------------------------------------------------    
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // e.hash & (newCap - 1)为计算数据在新的tab上的下标
                // 如果e的下一个节点为空,则直接复制到新的tab上
                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;
                    }
                }
            }
        }
    }
    // 返回newTab
    return newTab;
}

上述中分界线后的代码即是将数据从旧tab复制到新tab中的操作。

在这个操作过程中,创建了两个新的链表:lo链表和li链表;

链表的拆分

在遍历第j个链表的过程中,按照当前的节点是否满足(e.hash & oldCap) == 0的条件将节点添加到lo链表(满足条件)和li链表(不满足条件)。

最后会将lo链表添加到newTab的j位置上,将li链表添加到newTab的j+oldCap位置上。

从集合中获取key的value值

**get(Object key)**函数

public V get(Object key) {
    Node<K,V> e;
    // 计算key的hash值,通过hash值和key获取node节点,如果为空返回空,否则返回node的value值
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

**getNode(int hash, Object key)**函数

final Node<K,V> getNode(int hash, Object key) {
    // 判断table数组是否为空、tab的长度是否大于0、根据hash值计算对应下标,获取对于节点。
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果first的hash值与hash值相等且first的key与key相等,则直接返回当前节点。
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果first的下一个节点不为空
        if ((e = first.next) != null) {
            // 如果first节点为树形节点
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 遍历链表知道e的下一个节点为空
                // 如果满足以下条件则返回e节点
                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、付费专栏及课程。

余额充值