集合及源码分析

1.集合

  • 数组:

    • 长度开始需要指定,且指定后无法修改
    • 保存的必须为同一类型元素
    • 增/删元素时很麻烦,需要新建数组再重新赋值
  • 集合:

    • 可以动态保存任意多个对象
    • 提供一系列方便的操作对象的方法
    • 增/删元素时更加方便
  • 集合框架体系:

    • 单列集合Collection:以单个元素存在,比如list.add("psj")

    在这里插入图片描述

  • 双列集合Map:以键值对形式存在,比如hashMap.put(1,"psj")

在这里插入图片描述


2.Collection

2.1 基本特性
  • Collection接口实现的子类可以存放多个元素,每个元素可以是object
List list = new ArrayList();
list.add("psj");
list.add(10);
System.out.println(list);  // [psj, 10]
  • Collection接口没有直接实现子类,而是通过子接口SetList实现

tips:

  • 如果集合中有多个重复元素,remove方法移除的是集合中首先出现的元素,不会将所有元素移除
2.2 遍历元素
  • 使用Iterator

    • 所有实现Collection接口的集合类都有一个iterator方法,用于返回一个迭代器
    • Iterator仅用于遍历集合,本身不存放对象
    List list = new ArrayList();
    list.add("psj");
    list.add(10);
    list.add(true);
    Iterator iterator = list.iterator();  // 最开始的位置在集合首个元素的上方
    while (iterator.hasNext()){
        // 执行next方法后将iterator下移,并将下移后集合位置上的元素返回(返回类型是object)
        System.out.println(iterator.next());  
    }
    
  • 使用增强for循环:

    • 底层依旧使用Iterator
    List list = new ArrayList();
    list.add("psj");
    list.add(10);
    list.add(true);
    for (Object o : list) {
        System.out.println(o);
    }
    
2.3 List
  • List接口的集合类中元素有序,即添加顺序和取出顺序一致,且可以重复
  • List接口的集合类中每个元素都有对应的顺序索引
List list = new ArrayList();
list.add("psj");
list.add(10);
list.add("psj");
list.add(true);
for (Object o : list) {
    System.out.println(o);
}  //psj 10 psj true

2.3.1 ArrayList
  • 可以存放空值:
List list = new ArrayList();
list.add(null);
System.out.println(list);  // [null]
  • 底层由数组实现存储
  • 线程不安全(没有synchronized关键字),但是执行效率高
  • 维护了一个Object类型的数组elementData
  • 创建对象时如果使用无参构造器,则初始elementData容量为0,第一次添加元素会将elementData扩容为10,再次扩容则将其容量扩容为当前数组大小的1.5倍;使用指定大小的构造器,初始容量为指定大小,扩容后容量变为当前数组大小的1.5倍
// 使用无参构造器
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  // DEFAULTCAPACITY_EMPTY_ELEMENTDATA为{}
}
// 执行add方法
private void add(E e, Object[] elementData, int s) {
    // 数组还没有添加元素时
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}
// 第一次添加元素时扩容
private Object[] grow(int minCapacity) {  // minCapacity表示此时添加元素需要的数组大小
    return elementData = Arrays.copyOf(elementData,
                                       newCapacity(minCapacity));  // 使用的copyOf保证扩容后原数据不会丢
}
// newCapacity方法
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
    return Math.max(DEFAULT_CAPACITY, minCapacity);  // 第一次添加元素扩容为10的原因,DEFAULT_CAPACITY=10
int newCapacity = oldCapacity + (oldCapacity >> 1);  // 扩容1.5倍的原因

tips:

  • transient表示短暂的,被其修饰的属性不会被序列化
  • ArrayList中元素的添加/删除通过数组完成(即涉及到扩容),所以添加/删除效率较低,但是改/查的效率较高
2.3.2 Vector
  • 底层由数组实现存储
  • 维护了一个Object类型的数组elementData
  • 线程安全(有synchronized关键字),但是效率不高
  • 创建对象时如果使用无参构造器,则初始elementData容量为0,第一次添加元素会将elementData扩容为10,再次扩容则将其容量扩容为当前数组大小的2倍;使用指定大小的构造器,初始容量为指定大小,扩容后容量变为当前数组大小的2倍
// 扩容2倍的原因,capacityIncrement表示在构造器中指定每次扩容的数量大小
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                 capacityIncrement : oldCapacity);
2.3.3 LinkedList
  • 实现了双向链表和双端队列特点
  • 底层维护了一个双向链表
  • 属性包括一个first指针和一个last指针
  • 线程不安全

tips:

  • LinkedList中元素的添加/删除不是通过数组完成(即不涉及到扩容),所以添加/删除效率较高,但是改/查的效率较低
  • LinkedList添加时采用尾插法,删除时默认删除第一个节点
2.4 Set
  • 无序(添加和取出的顺序不一致,但是取出的顺序是固定的),没有索引(即不能使用索引的方式遍历Set集合)
HashSet set = new HashSet();
set.add("psw2");
set.add(null);
set.add("psj");
set.add("psw");
set.add(null);
System.out.println(set);  // 输出[null, psw, psj, psw2],调换添加顺序或者再次运行输出内容都是一样的
  • 不允许重复元素(所以最多包含一个null),但是在怎样算重复元素和add方法的底层原理有关:
HashSet set = new HashSet();
set.add(new Person("psj"));
set.add(new Person("psj"));
System.out.println(set);  // [psj, psj]
//  Person类
class Person{
    private String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}
2.4.1 HashSet
  • HashSet实际上是HashMapHashMap的底层是数组+链表+红黑树,table[0...]上也是存放元素的,并不是把元素全部存放在链表上):

在这里插入图片描述

public HashSet() {
    // 执行new HashMap<>()会初始化加载因子loadfactor=0.75,且table为null,添加元素时才会执行resize方法扩容
    map = new HashMap<>();
}
  • add方法的底层机制:
    • 添加元素时会先得到hash值,再将该值转为索引值
    • 查看table的索引位置上是否存放元素:没存放就直接加入;存放了就调用equals方法比较,相同就不添加,不同就添加到链表最后
    • 链表的元素个数达到TREEIFY_THRESHOLD(默认为8)并且table的大小达到MIN_TREEIFY_CAPACITY(默认为64),就进行树化,否则依旧采用数组扩容机制
public boolean add(E e) {
    return map.put(e, PRESENT)==null;  // **PRESENT是final static修饰的对象,不会改变,起到占位的作用
}
// add方法中的put方法,这是HashMap中定义的方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
// hash方法会调用key的hashCode方法,但是并不等于hashCode的值(为了尽量避免碰撞)
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// putVal方法解析
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table是一个Node数组(如上图的数组)
    if ((tab = table) == null || (n = tab.length) == 0)
        // 在resize方法中声明了初始的table大小newCap为16,newThr=0.75*16=12,即当table中元素到了12个就要开始			准备扩容,防止一次性增加大量元素时出现卡顿
        // newCap = DEFAULT_INITIAL_CAPACITY;
        // newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        // resize方法最后返回一个初始化后的table
        n = (tab = resize()).length;
    // 根据key的hash值计算key应该存放在table中的位置,并将该位置的对象赋值给p
    // 如果该位置的对象为null,就创建Node并放置在该位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 如果该位置不为null,即碰巧不同对象计算i = (n - 1) & hash后的值相同,就要开始走链表或者红黑树
    else {
        Node<K,V> e; K k;
        // 如果当前数组位置对应的链表的第一个位置和准备添加的key对象的hash值一样
        // 并且满足下面两个条件之一:
        // (1) 准备添加的key对象和p指向的Node的key对象是同一个对象
        // (2) p指向的Node的key对象和准备添加的key对象使用equals方法比较后相同
        // 就不能加入,即不再遍历链表上的元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 判断p是不是红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 走到该步则比较的是当前数组位置对应的链表的第二个位置及之后的元素和准备添加的key对象
         	// 如果对应的数组位置已经是一个链表,就使用for循环依次比较:
            // (1)不相同就加入链表尾部,加入完后就break
            // (2)相同就break,不会添加
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果链表的长度达到TREEIFY_THRESHOLD就将链表进行树化判断
                    // (treeifyBin方法会先将table扩容,如果扩容后的table过大再进行树化)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    // 返回null表示添加成功
    return null;
}

tips:

  • HashSet第一次添加元素时,table扩容到16,临界值为16*0.75=12。如果table中的元素个数达到12后(元素个数并不单指占据数组位置的个数,包括在链表上的元素个数),就扩容到16*2=32,临界值为32*0.75=24,依次类推
  • table扩容有两种情况:
    • table中使用位置个数到达临界值
    • table某个位置上的链表到达TREEIFY_THRESHOLD,之后每有一个元素到该链表上table就扩容(链表在table上的位置会发生改变)
  • 两个对象经过hashcode方法后得到的值不相同,但是经过hash方法的(h = key.hashCode()) ^ (h >>> 16)后的值可能相同,此时就会放置在table数组上的同一个链表(能否放置还要继续用equals方法判断)。假设要保证自定义对象属性值一样就在添加时一定无法加入,就要重写equals方法和hashCode方法:
HashSet set = new HashSet();
set.add(new Person("psj"));
set.add(new Person("psj"));
System.out.println(set);  // 只打印[psj],如果只重写equals方法或者hashCode方法会输出[psj,psj]
// 重写方法
class Person{
    private String name;
    public Person(String name) {
        this.name = name;
    }
    // 判断值
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }
    // 不同的对象使用原始的hashCode得到的值一定不相同,所以需要重写该方法,保证其值是根据的是属性值判断
    // 属性值相同得到的hash值就相同,而不关心是否为同一个对象
    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}
  • 对于HashSetremove方法也是依据hash值进行删除的:
// Person类重写了hashcode和equals方法
class Person{
    public String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
HashSet set = new HashSet();
Person p1 = new Person("psj", 11);
Person p2 = new Person("psw", 12);
set.add(p1);
set.add(p2);
p1.name = "psj2";
set.remove(p1);  
System.out.println(set);  // 还是输出两个对象:p1对象的name值变了,p1对象的hash值也会变化(原始的hashcode只是根据对象的地址而定),执行remove根据当前p1的hash值找table表时发现没有该元素,所以删除失败
set.add(new Person("psj2", 11));
System.out.println(set);  // 输出三个对象:尽管新建的对象和修改后p1的属性值一样,但是p1在table表的位置和原始位置一致,并没有占据新建对象的位置,所以新建的对象可以添加到table表中
set.add(new Person("psj",11));
System.out.println(set);  // 输出四个对象:该对象和未修改前的p1计算的hash值一样,所以会判断是否能放置在p1所在链表上,但是由于name和修改后的p1不一样,根据重写的equals方法判断可以添加到链表中
2.4.2 LinkedHashSet
  • 它是HashSet的子类:

  • 底层是一个LinkeHashMap,底层维护了数组+双向链表

  • 根据hashCode值决定元素存储位置,同时使用链表维护元素次序,使得元素看起来是以插入顺序保存的(即插入顺序和遍历顺序一致):

在这里插入图片描述

Set set = new LinkedHashSet();
set.add(456);
set.add(456);
set.add(123);
set.add(new String("psw"));
set.add(new String("psj"));
System.out.println(set);  // 打印[456, 123, psw, psj]
  • add方法的底层机制:

    • 第一次添加元素时,table数组扩容到16,且table数组的类型为HashMap$Node,但是存放的节点类型为LinkeHashMap$Entry,说明EntryHashMap的静态内部类Node的子类:
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    
    • 在执行add方法时还是去执行HashSetadd方法,:
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;  // PRESENT是final static修饰的对象,不会改变,起到占位的作用
    }
    
2.4.3 TreeSet
  • TreeSet底层是TreeMap,但是使用add方法时value值依旧是固定的PRESENT
public TreeSet() {
   this(new TreeMap<>());
}
public boolean add(E e) {
   return m.put(e, PRESENT)==null;
}
  • 要实现指定排序方式,可以使用TreeSet提供的构造器,在其中传入一个比较器:
TreeSet set = new TreeSet(new Comparator() {
   @Override
   public int compare(Object o1, Object o2) {
       return ((String)o2).compareTo((String)o1);
   }
});
set.add("tom");
set.add("jack");
set.add("psj");
set.add("aim");
set.add("bill");  
System.out.println(set);  // [tom, psj, jack, bill, aim]

tips:

  • 不同于HashMap根据hash值确定能否添加成功,TreeSet能否成功添加元素和定义的比较器有关,不在于添加的元素是否相等:
TreeSet set = new TreeSet(new Comparator() {
   @Override
   public int compare(Object o1, Object o2) {
       return ((String) o2).length() - ((String) o1).length();
   }
});
set.add("tom");
set.add("jack");
set.add("psj");
System.out.println(set);  // 只会打印[jack, tom]
// 尽管tom和psj是两个不同的字符串对象,但是根据TreeMap中的put方法:
if (cpr != null) {
   do {
       parent = t;
       cmp = cpr.compare(key, t.key);
       if (cmp < 0)
           t = t.left;
       else if (cmp > 0)
           t = t.right;
       else
           return t.setValue(value);  // 判断完这两个字符串长度一致后会执行该行代码,不会再往下执行,即不会添加
   } while (t != null);
}
...
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
   parent.left = e;
else
   parent.right = e;
  • 如果使用无参构造器创建TreeSet对象,会调用key值类型的比较器进行比较:
// TreeMap中的put方法
if (key == null)
   throw new NullPointerException();
@SuppressWarnings("unchecked")
   Comparable<? super K> k = (Comparable<? super K>) key;  // k对象的运行类型还是key所属的类型
do {
   parent = t;
   cmp = k.compareTo(t.key);
   if (cmp < 0)
       t = t.left;
   else if (cmp > 0)
       t = t.right;
   else
       return t.setValue(value);
} while (t != null);
  • 在自定义类中如果没有实现Comparable接口,将该类的对象添加到TreeSet中会报异常:
// A类
class A{}
public static void main(String[] args) {
   TreeSet set = new TreeSet();
   set.add(new A());  // 报ClassCastException异常,
}
// 执行add方法会执行到下面方法,因为A类没有实现Comparable接口,所以执行((Comparable<? super K>)k1)会报类型转换异常(将一个类转为毫无关系的接口肯定报异常)
final int compare(Object k1, Object k2) {
   return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
       : comparator.compare((K)k1, (K)k2);
}

3.Map

3.1 基本特性
  • Map中的keyvalue可以是任何引用类型,会封装到HashMap$Node
  • Map中的key不允许重复,如果有重复的key添加会将新的value进行替换
  • Map中的value可以重复
  • Map中的keyvalue都可以为null,但是key只能有一个为nullvalue可以有多个

tips:

  • HashSet的底层是HashMap,只不过把键值对的value固定为PRESENT
  • HashMap的插入顺序和遍历顺序不一致(分析HashSet源码可以得知)
  • 为了方便遍历,会创建EntrySet集合,存放类型为接口Entry<K,V>,把HashMap$Node(Entry的实现类)对象存放到EntrySet中方便遍历是因为Map.Entry提供了getKeygetValue方法,所以Map中的keysvalues并不是直接分别放在Set集合和Collection集合中,还是通过遍历Map.Entry得到keyvalue值:
Map map = new HashMap();
map.put(new A("psj"), "psj");
map.put(new A("psw"), "psw");
Set entrySet = map.entrySet();
for (Object obj : entrySet) {
    Map.Entry entry = (Map.Entry) obj;
    System.out.println(entry.getKey());
    System.out.println(entry.getValue());
}
// 在下面两个方法中都有该方法:public final void forEach(Consumer<? super V> action)
Set keys = map.keySet();
Collection values = map.values();
3.2 遍历元素
  • 取出所有的key值,通过key值得到value
Map map = new HashMap();
map.put(1, "psj");
map.put(2, "psw");
map.put(3, "psj2");
map.put(4, "psw2");
Set keySet = map.keySet();
// 使用增强for循环
for (Object key : keySet) {
    System.out.println(map.get(key));
}
// 使用迭代器
while (iterator.hasNext()){
    Object key = iterator.next();
    System.out.println(map.get(key));
}
  • 直接取出所有的value
Collection values = map.values();
// 使用增强for循环
for (Object value : values) {
    System.out.println(value);
}
// 使用迭代器
Iterator iterator = values.iterator();
while (iterator.hasNext()){
    System.out.println(iterator.next());
}
  • 通过EntrySet获取K-V对:
Set entrySet = map.entrySet();
for (Object entry : entrySet) {
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey());
    System.out.println(m.getValue());
}
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
    //正常情况下应该要向下转型为HashMap$Node类型,但是该内部类Node为默认访问修饰符,无法访问,只能转为接口		  Entry(Node实现了该接口),但是运行类型还是Node
    Map.Entry entry = (Map.Entry) iterator.next(); 
    System.out.println(entry.getKey());
    System.out.println(entry.getValue());
}
3.3 HashMap
  • HashMap没有实现同步,所以线程不安全(没有synchronized关键字)
  • 扩容机制和HashSet一致(具体参考2.4.1源码分析)
  • key值相同时会走该步判断进行value值的替换:
if (e != null) { // existing mapping for key
	V oldValue = e.value;
	if (!onlyIfAbsent || oldValue == null)
		e.value = value;
	afterNodeAccess(e);
	return oldValue;
}
  • 当桶中节点数由于移除或者 resize (扩容) 变少后,红黑树会转变为普通的链表,这个阈值是UNTREEIFY_THRESHOLD(默认值6)
  • 红黑树虽然查询效率比链表高,但是结点占用的空间大,只有达到一定的数目才有树化的意义
3.4 Hashtable
  • Hashtable存放的key值和value值都不能存放null,会抛出空指针异常
  • Hashtable线程安全(方法中有synchronized关键字),但是效率没HashMap
  • 不同于HashMap初始化时tablenull,在添加元素时执行resize方法才将数组大小扩容为16,Hashtable初始化生成的table数组大小为11,同时也会初始化加载因子:
// Hashtable初始化
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}
// HashMap初始化
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR;
}
  • 扩容机制和HashMap类似,当到达阈值threshold=initialCapacity * loadFactor进行扩容(并不是table大小超过阈值才扩容,HashMap需要超过阈值才扩容):
int newCapacity = (oldCapacity << 1) + 1;  // 扩容后的table大小,不同于HashMap直接乘以2,而是再加上1
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); // 阈值计算方式一样
  • HashMaptable数组类型为Node,而HashtableEntry
3.5 Properties
  • 继承Hashtable类,并且实现了Map接口
  • 主要运用在从xxx.properties配置文件中加载数据到Properties类的对象,然后进行读取和修改
  • 存放的key值和value值都不能存放null,会抛出空指针异常
  • HashMapHashtable一样,添加和取出的顺序不一致,但是取出的顺序是固定的,并且没有索引(即不能使用get(index)的方式遍历)
3.6 TreeMap
  • 要实现指定排序方式,可以使用TreeMap提供的构造器,在其中传入一个比较器(具体操作可以参考2.4.3)

tips:

  • 如果使用无参构造器创建TreeMap对象,会调用key值类型的比较器进行比较:
// put方法
if (key == null)
    throw new NullPointerException();
@SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key; // k对象的运行类型还是key所属的类型
do {
    parent = t;
    cmp = k.compareTo(t.key);
    if (cmp < 0)
        t = t.left;
    else if (cmp > 0)
        t = t.right;
    else
        return t.setValue(value);
} while (t != null);

4.如何选择集合实现类

  • 判断存储的类型
    • 一组对象使用Collection接口
    • 一组键值对使用Map接口
  • 对于Collection接口:
    • 允许重复使用List接口:
      • 增/删操作多:LinkedList(底层维护了一个双向链表)
      • 查/改操作多:ArrayList(底层维护了一个可变数组)
    • 不允许重复使用Set接口:
      • 无序:HashSet(底层是HashMap,维护了一个哈希表,即数组+链表+红黑树)
      • 有序:TreeSet(底层是TreeMap,通过定义比较器可以自定义排序规则)
      • 插入和取出顺序一致:LinkedHashSet(底层是LinkedHashMap,维护数组+双向链表)
  • 对于Map接口:
    • 键无序:HashMap(底层维护了一个哈希表,即数组+链表+红黑树,jdk8之前为数组+链表)
    • 键有序:TreeMap
    • 键插入顺序和取出顺序一致:LinkedHashMap(底层是HashMap)
    • 读取文件:Properties

5.Collections工具类

  • Collections是一个操作SetListMap等集合的工具类
  • 提供了一系列静态方法对集合元素进行排序、查询和修改等操作:
ArrayList list = new ArrayList();
list.add("1");
list.add(2);  // 打印
System.out.println(list); // [1, 2]
Collections.reverse(list);
System.out.println(list); // [2, 1]

tips:

  • 使用Collectiossortmax等方法时,需要保证集合内存放的元素是相同类型的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值