JavaSe之泛型

1、引入泛型

之前咱们的超级数组中只能存数字,不能存其他类型的数据,是不是还是显得有些鸡肋。那我们能不能改进一下,让它可以存任意类型。

第一种解决方案:

将内部的int数据,换成Object类型,使用引用数据类型替代基础数据类型。因为Object是所有类的超类,所以任何子类都可以传递进去,我们的代码可以简化如下:

package com.ydlclass;
public class SuperArray {
    private Object[] array;
    //根据下标查询数字

    //当前最后一个数字的下边,要为-1 ,以为数组的第一个下标为0
    private int currentIndex = -1;

    //构造是初始化
    public SuperArray(){
        array = new Object[8];
    }

    //添加数据的方法
    public void add(Object data){
        System.out.println("我是数组的实现!---add");
        currentIndex++;
        //自动扩容
        if(currentIndex > array.length-1){
            array = dilatation(array);
        }
        array[currentIndex] = data;
    }

    public Object get(int index){
        System.out.println("我是数组的实现---get");
        return array[index];
    }

    //数组扩容的方法
    private Object[] dilatation(Object[] oldArray){
        Object[] newArray = new Object[oldArray.length * 2];
        for (int i = 0; i < oldArray.length; i++) {
            newArray[i] = oldArray[i];
        }
        return newArray;
    }

    //验证下标是否合法
    private boolean validateIndex(int index) {
        //只要有一个不满足就返回false
        return index <= currentIndex && index >= 0;
    }
}
public static void main(String[] args) {
    SuperArray superArray = new SuperArray();
    superArray.add("abc");
    String item = (String)superArray.get(0);
}

思考这样会有什么问题吗?

1、我们规定传入的对象只要是Object子类就行,那就意味着,所有的对象都可以往篮子里扔,可以扔水果,也可以扔炸弹,数据类型不能很好的统一。

2、从超级数组中获取数据后必须强转才能使用,这是不是意味着,极有可能发生ClassCastException。

SuperArray superArray = new SuperArray();
superArray.add(new Date());
superArray.add(new Dog());

(Dog)superArray.get(0);

那怎么解决这个问题呢?

我们的目标是:1、能够规定传入的数据类型必须是我们要求的。

2、从数组中获取的数据必须是确定的类型。

【泛型】就能够很好的解决这个问题。

2、泛型的定义

什么是泛型?

看表面的意思,泛型就是指广泛的、普通的类型。泛型能够帮助我们把【类型明确】的工作推迟到创建对象或调用方法的时候。

意思就是:我定义类的时候不用管到底是什么类型,new这个对象或者调用这个对象的方法时才确定具体的类型。

这听起来很不可思议,那到底是什么意思呢?咱们用以下的例子来说明情况:

(1)泛型类

泛型类也就是把泛型定义在类上,这样用户在使用类的时候才把类型给确定下来。

具体的方法就是使用<>加一个未知数,通常用 T K V 等大写字符表示,事实上只要是个单词就可以。

package com.ydlclass;
// 加上<T>之后,表示以后的SuperArray只能存某种类型,但是这个类型暂时不确定,使用T来代替
public class SuperArray<T> {
    private Object[] array;
    //根据下标查询数字

    //当前最后一个数字的下边,要为-1 ,以为数组的第一个下标为0
    private int currentIndex = -1;

    //构造是初始化
    public SuperArray(){
        array = new Object[8];
    }

    //添加数据的方法
    public void add(T data){
        System.out.println("我是数组的实现!---add");
        currentIndex++;
        //自动扩容
        if(currentIndex > array.length-1){
            array = dilatation(array);
        }
        array[currentIndex] = data;
    }

    public T get(int index){
        System.out.println("我是数组的实现---get");
        return (T)array[index];
    }

    //数组扩容的方法
    private Object[] dilatation(Object[] oldArray){
        Object[] newArray = new Object[oldArray.length * 2];
        for (int i = 0; i < oldArray.length; i++) {
            newArray[i] = oldArray[i];
        }
        return newArray;
    }

    //验证下标是否合法
    private boolean validateIndex(int index) {
        //只要有一个不满足就返回false
        return index <= currentIndex && index >= 0;
    }
}

有了上边的代码,我们需要怎么去定义一个超级数组呢?

public static void main(String[] args) {
    // jdk1.7以前
    SuperArray<String> superArray = new SuperArray<String>();
    // jdk1.7以后提出了钻石语法,可以进行类型的自动推断,后边的尖括号就不用写了
    SuperArray<String> superArray = new SuperArray<>();
    superArray.add("abc");
    String item = superArray.get(0);
}

我们申明一个类的时候,无需关心将来我的超级数组存的是什么类型,但是new SuperArray的时候明确指出了这个超级数组只能存String类型的,其他类型就不能存。

可以看到上面这个程序,在使用时如果定义了类型,那么在使用时就可以不用进行强制类型转换,直接就可以得到一个T类型的对象。

(2)泛型方法

有时候只关心某个方法,那么使用泛型时可以不定义泛型类,而是只定义一个泛型方法,如下:

public class Test2 {
    public <T> T show(T t) {
        System.out.println(t);
        return t;
    }

    public static <T> T show2(T one) { //这是正确的
        return null;
    }

    public static void main(String[] args) {
        Test2 test2 = new Test2();
        String show = test2.show("123");
        Integer show1 = test2.show(123);
    }
}

需要注意一下定义的格式,泛型必须得先定义才能够使用。

说明一下,定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。

泛型方法最好要结合具体的返回值,否则和Object作为参数差别不大。

我们在学习反射的时候会学习类似的例子。

(3)继承关系

泛型类在继承时,可以明确父类(泛型类)的参数类型,也可以不明确。 还记得我们学习策略设计模式时的Comparator接口吗?之前的设计必须是User类型,而现在有了泛型我们可以灵活使用了:

// 泛型类
public interface Comparator<T>{
    int compare(T o1, T o2);
}
public class StudentComparator implements Comparator {
    @Override
    public Integer compare(Object o1, Object o2) {
        if(o1 instanceof Student && o2 instanceof Student){
            return ((Student) o1).getAge() - ((Student) o2).getAge();
        }
        return null;
    }
}
(1)明确类型,子类存在的目的就是比较User对象
package com.ydlclass;

public class User {
    private String name;
    private int age;
    private int height;

    public User() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}
//在实现泛型类时明确父类的类型
import java.util.Comparator;

public class UserAgeComparator implements Comparator<User> {
    @Override
    public int compare(User o1, User o2) {
        return o1.getAge() - o2.getAge();
    }
}

public static void main(String[] args) {
    UserAgeComparator userAgeComparator = new UserAgeComparator();
    int compare = userAgeComparator.compare(new User(), new User());
    System.out.println(compare);
}
(2)不明确类型

子类不去明确类型,明确类型的工作留在创建对象的时候:

public class UserAgeComparator<T> implements Comparator<T> {

    @Override
    public int compare(T o1, T o2) {
        return o1.equals(o2) ? 0 : 1;
    }

    public static void main(String[] args) {
        UserAgeComparator<User> userAgeComparator = new UserAgeComparator<>();
        int compare = userAgeComparator.compare(new User(), new User());
        System.out.println(compare);
    }
}

3、项目实战

用泛型改进超级数组和超级链表,这里只有部分代码:

package com.ydlclass;

public interface Super<T> {

    /**
     * 标记所有的子类实现必须有add方法,添加数据
     * @param data
     */
    void add(T data);
    
    /**
     * 标记所有的子类实现必须有get方法,获取数据
     * @param index
     * @return
     */
    T get(int index);

    /**
     * 标记所有的子类实现必须有size方法,数据大小
     * @return
     */
    int size();
}
package com.ydlclass;

public class SuperArray<T> implements Super<T> {

    //维护一个数组,要想什么都存,就要使用顶级父类
    private Object[] array;
    //当前最后一个数字的下边,要为-1 ,以为数组的第一个下标为0
    private int currentIndex = -1;

    //构造是初始化
    public SuperArray(){
        array = new Object[8];
    }

    //添加数据的方法
    public void add(T data){
        System.out.println("我是数组的实现!---add");
        currentIndex++;
        //自动扩容
        if(currentIndex > array.length-1){
            array = dilatation(array);
        }
        array[currentIndex] = data;
    }


    //根据下标查询数字
    public T get(int index){
        System.out.println("我是数组的实现---get");
        return (T)array[index];
    }

    //查看当前有多少个数字
    public int size(){
        return currentIndex + 1;
    }

    //数组扩容的方法
    private Object[] dilatation(Object[] oldArray){
        Object[] newArray = new Object[oldArray.length * 2];
        for (int i = 0; i < oldArray.length; i++) {
            newArray[i] = oldArray[i];
        }
        return newArray;
    }

    //验证下标是否合法
    private boolean validateIndex(int index) {
        //只要有一个不满足就返回false
        return index <= currentIndex && index >= 0;
    }
}
package com.ydlclass;

/**
 * @author itnanls
 * @date 2021/7/16
 **/
public class SuperLinked<T> implements Super<T> {

    private Node head = null;
    private Node tail = null;

    private int length = 0;

    //添加元素
    public void add(T data){
        System.out.println("我是链表的实现-----add");
        Node<T> node = new Node<>();
        node.setNum(data);
        if (length == 0) {
            //如果第一次添加一共就一个节点
            head = node;
        }else{
            //和尾巴拉手
            tail.setNextNode(node);
        }
        //把新添加进来的当成尾巴
        tail = node;
        length ++;
    }

    //根据下标查询数字,非常有意思的写法
    public T get(int index){
        System.out.println("我是链表的实现------get");
        if(index > length){
            return null;
        }
        //小技巧
        Node targetNode = head;
        for (int i = 0; i < index; i++) {
            targetNode = targetNode.getNextNode();
        }
        return (T)(targetNode.getNum());
    }

    //查看当前有多少个数字
    public int size(){
        return length;
    }

    class Node<T> {

        //存储的真实数据
        private T num;

        //写一个节点
        private Node nextNode = null;

        public T getNum() {
            return num;
        }

        public void setNum(T num) {
            this.num = num;
        }

        public Node getNextNode() {
            return nextNode;
        }

        public void setNextNode(Node nextNode) {
            this.nextNode = nextNode;
        }
    }
}

4、类型通配符

新建三个类:

public class Animal {
}
public class Dog extends Animal {
}
public class Teddy extends Dog {
}

当我们的一个方法的参数需要传入一个带有参数的类型的时候,可以使用通配符来确定具体传入的对象范围。

public static void print(Comparator<Object> comparator){

}

public static void main(String[] args) {
    Comparator<User> comparator = new Comparator<User>(){
        @Override
        public int compare(User o1, User o2) {
            return o1.getAge() - o2.getAge();
        }
    };
    print(comparator);
}

会有以下的报错信息:

image-20210829092542407

意思就是我需要一个泛型是Object的Comparator但你提供的泛型是User,使用通配符就能很好的解决这些问题:

(1)无界

类型通配符我感觉上和泛型方法差不多,只是不用在使用前进行定义,例子如下:

public static void main(String[] args) {
    SuperArray<Dog> superArray = new SuperArray<>();
    superArray.add(new Dog());
    superArray.add(new Teddy());
    printSuperArray(superArray);
}

public static void printSuperArray(SuperArray<?> superArray){
    for (int i = 0;i<superArray.size();i++){
        System.out.println(superArray.get(i));
    }
}

"?"可以接收任何类型,有些聪明的小伙伴可能发现不加?,连泛型也不要行不行,悄悄的告诉你,可以,但是程序会报一个警告:

public static void print(Comparator comparator){  }
Raw use of parameterized class ‘xxxx‘ 警告
没有类型参数的泛型

使用原始类型(没有类型参数的泛型)是合法的,但是你永远不应该这样做。如果使用原始类型,就会失去泛型的安全性和表现力。 既然你不应该使用它们,那么为什么语言设计者一开始就允许原始类型呢?答案是:为了兼容性。Java 即将进入第二个十年,泛型被添加进来时,还存在大量不使用泛型的代码。保持所有这些代码合法并与使用泛型的新代码兼容被认为是关键的。将参数化类型的实例传递给设计用于原始类型的方法必须是合法的,反之亦然。

(2)上界

我们可以使用(SuperArray<? extends Dog> superArray)的形式来约定传入参数的上界,意思就是泛型只能是Dog的或者Dog的子类。

  public static void main(String[] args) {
        SuperArray<Animal> superArray = new SuperArray<>();
        superArray.add(new Dog());
        superArray.add(new Teddy());
        superArray.add(new Animal());
        printSuperArray(superArray);
    }
    
    public static void printSuperArray(SuperArray<? extends Dog> superArray){
        for (int i = 0;i<superArray.size();i++){
            System.out.println(superArray.get(i));
        }
    }

这种情况下能够接收A类或者A类的子类。

image-20210716171430366

注:当我们使用extends时,我们可以读元素,因为元素都是A类或子类,可以放心的用A类拿出。

(3)下界

我们可以使用(SuperArray<? super Dog> superArray)的形式来约定传入参数的下界,意思就是泛型只能是Dog的或者Dog的超类。

public static void main(String[] args) {
    SuperArray<Teddy> superArray = new SuperArray<>();
    superArray.add(new Teddy());
    printSuperArray(superArray);
}
public static void printSuperArray(SuperArray<? super Dog> superArray){
    for (int i = 0;i<superArray.size();i++){
        System.out.println(superArray.get(i));
    }
}

image-20210716171612067

当使用super时,可以添加元素,因为都是A类或父类,那么就可以安全的插入A类。

5、类型擦除

我们刚刚讲过,为了兼容性,使用原始类型(没有类型参数的泛型)是合法的,泛型被添加进来时,还存在大量不使用泛型的代码。保持所有这些代码合法并与使用泛型的新代码兼容被认为是关键的。将参数化类型的实例传递给设计用于原始类型的方法必须是合法的,反之亦然。

为了保持这种兼容性,Java的泛型其实是一种伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除

如在代码中定义SuperArray<Object>SuperArray<String>等类型,在编译后都会变成SuperArray,JVM看到的只是SuperArray,而由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现完全避免类型转换异常的情况。

(1)泛型不能是基本数据类型

不能用类型参数替换基本类型。就比如,没有SuperArray<double>,只有SuperArray<Double>。因为当类型擦除后,SuperArray的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

image-20210829094422946

这一点尤其重要:必须要记住。

(2)重载方法

如果泛型类型因为具体的泛型不同而导致方法签名不同,那么以下两个方法就是两种重载方法:

public static void print(Comparator<Object> comparator){

}

public static void print(Comparator<User> comparator){

}

然而事实上:

image-20210829094136772

因为泛型被擦除后,其实这两个方法是一致的,并不能构成泛型。

(3)类型擦除和多态的冲突

现在有这样一个泛型类:

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T value) {  
        this.value = value;  
    }  
}

然后我们想要一个子类继承它。

package com.ydlclass;

import java.util.Date;

public class DatePair extends Pair<Date>{
    @Override
    public Date getValue() {
        return super.getValue();
    }

    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }
}

在这个子类中,我们设定父类的泛型类型为Pair<Date>,在子类中,我们重写了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。

所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}  
{
  public com.ydlclass.Pair();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Pair;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Pair<TT;>;

  public T getValue();
  	// 这里我们知道这个方法的返回值是Object
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field value:Ljava/lang/Object;
         4: areturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Pair;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Pair<TT;>;
    Signature: #20                          // ()TT;

  public void setValue(T);
  	// 这里我们知道这个方法的参数是引用数据类型,Object
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field value:Ljava/lang/Object;
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/ydlclass/Pair;
            0       6     1 value   Ljava/lang/Object;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/ydlclass/Pair<TT;>;
            0       6     1 value   TT;
    Signature: #23                          // (TT;)V
}
Signature: #24                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Pair.java"

再看子类的两个重写的方法的类型:

@Override  
public void setValue(Date value) {  
    super.setValue(value);  
}  
@Override  
public Date getValue() {  
    return super.getValue();  
}
{
  public java.util.Date getValue();
    descriptor: ()Ljava/util/Date;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method com/ydlclass/Pair.getValue:()Ljava/lang/Object;
         4: checkcast     #3                  // class java/util/Date
         7: areturn
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Lcom/ydlclass/DatePair;

  public void setValue(java.util.Date);
    descriptor: (Ljava/util/Date;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #4                  // Method com/ydlclass/Pair.setValue:(Ljava/lang/Object;)V
         5: return
      LineNumberTable:
        line 13: 0
        line 14: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/ydlclass/DatePair;
            0       6     1 value   Ljava/util/Date;


  // 桥接方法,一会分析
  public void setValue(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/util/Date
         5: invokevirtual #5                  // Method setValue:(Ljava/util/Date;)V
         8: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/ydlclass/DatePair;

  public java.lang.Object getValue();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #6                  // Method getValue:()Ljava/util/Date;
         4: areturn
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/DatePair;
}
Signature: #29                          // Lcom/ydlclass/Pair<Ljava/util/Date;>;
SourceFile: "DatePair.java"

先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果是在普通的继承关系中,根本就不会是重写,而是重载。

我们在一个main方法测试一下:

public static void main(String[] args) throws ClassNotFoundException {  
    DatePair DatePair = new DatePair();  
    DatePair.setValue(new Date());                  
    DatePair.setValue(new Object()); //编译错误  
}

如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,确实是重写了,而不是重载了。

关键字:ACC_BRIDGE, ACC_SYNTHETIC

从编译的结果来看,我们本意重写setValuegetValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的【桥方法】,我们从字节码中看到两个标志【ACC_BRIDGE, ACC_SYNTHETIC】。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而在我们自己定义的setvaluegetValue方法上面的@Override只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突

并且,还有一点也许会有疑问,子类中的桥方法Object getValue()Date getValue()是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分辨这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟机去区别。

6、静态方法和静态类中的问题

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

举例说明:

public class Test2<T> {    
    public static T one;   //编译错误    
    public static T show(T one){ //编译错误    
        return null;    
    }    
}

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

但是要注意区分下面的一种情况:

public class Test2<T> {    

    public static <T> T show(T one){ //这是正确的    
        return null;    
    }    
}

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值