java 泛型总结

前言

泛型机制是Java SE 5.0开始引入的,没有泛型之前,不同类型的对象重用相同的代码时,普遍使用Object变量,然后再进行强制类型转换。java中的ArrayList就是一个泛型类。假设自己实现一个ArrayList类CustomArrayList,里面用Object[]存储元素:

public class CustomArrayList {
    private Object[] elementData = new Object[10];
    private int index = 0;

    public void add(Object o){
        if(index < elementData.length)
            elementData[index++] = o;
    }

    public Object get(int i){
        if(i>=0 && i < elementData.length)
            return elementData[i];

        return null;
    }

    public int getLength(){
        return index;
    }
}

在调用get方法获取元素时就需要类型转换了。类型转换须与add方法添加的类型一致,否则就会导致异常。

在编译阶段并不检查强制类型转换的语法, 而转换究竟成功还是导致类型转换异常,只能在运行阶段才知晓。一个异常导致程序崩溃是非常不好的体验。如果能在编译就发现这类明显的错误显然要更好,这就是今天的主角—— 泛型
我们定义一个方法时,参数类型也同时定义,它们为形参。实际调用时,通过传入实参调用该方法。在这里,参数类型是确定的,只有符合该类型的实参才能成功调用该方法。在此基础上将参数类型抽象化,比如参数类型用T表示,它可以是Object及其任意子类。一旦T的具体类型确定,相关的方法类型也就确定了,编译器可以据此确定的类型来检查相关的错误。编写包含参数类型T的类和方法就是泛型编程(Generic programming)。泛型的本质就是参数类型化,又称类型参数(type parameters)。

//如果向该集合添加了非String对象,编译器很容易发现
ArrayList<String> strList = new ArrayList<>();

泛型类和泛型方法

泛型类

java中一般用T(必要时还可用临近的U、S)表示“任意类型”。用尖括号"<>"括起来,跟在类名的后面。类型变量T指定了方法的参数类型、返回类型、域的类型。来看一个简单的泛型类:

public class Pair<T> {
    private T min, max;

    public Pair(){
        min = null;
        max = null;
    }

    public Pair(T min, T max) {
        this.min = min;
        this.max = max;
    }

    public void setMax(T max) {
        this.max = max;
    }

    public void setMin(T min) {
        this.min = min;
    }

    public T getMax() {
        return max;
    }

    public T getMin() {
        return min;
    }

泛型类可以有多个类型变量,如 class P<T, U>{…}。
泛型类可看作普通类的工厂。实际使用时,只需用实际类型替换T即可。

泛型方法

泛型方法的类型变量须放在方法的修饰符后面,返回类型前面。泛型方法即可以定义在泛型类中,也可以定义在普通类中。

public class ArrayAlg<T> {
	 public T getMid(T... array) {
        return array[array.length/2];
    }

    public <T> T getMid2(T... array) {
        return array[array.length/2];
    }
}

ArrayAlg是一个泛型类,里面有两个成员方法。可知getMid2是一个泛型方法,getMid则不是,它只是泛型类的一个普通方法。需要明确的是,这里泛型方法getMid2中的T 与泛型类ArrayAlg的T分别代表两种类型,并没有什么关系。

静态泛型方法

在上面的类中添加一个静态方法:

public class ArrayAlg<T> {

    public static T getMiddle(T... array) {
        return array[array.length/2];
    }
 }   

编译报错:

com.milanac007.genericdemo.ArrayAlg.this cannot by referenced from a static context

泛型类中的带类型变量的静态方法,必须声明为泛型方法。 正确的写法为:

public static <T> T getMiddle(T... array) {
        return array[array.length/2];
    }

假设如下调用上面的静态方法:

double middle = ArrayAlg.getMiddle(3.14, 2 ,3);

编译报错:
在这里插入图片描述
编译器自动打包参数为一个Double和两个Integer对象,然后寻找它们共同的超类型。实际上找到了两个超类型:Nubmer和Comparable。
补救:让参数类型或它的超类型有且只有一个。

方法一 将所有实参改为double:

double middle = ArrayAlg.getMiddle(3.14, 2.0 ,3.0);

方式二 接收数据类型改为NumberComparable:

Number middle = ArrayAlg.getMiddle(3.14, 2 ,3);

类型变量的限定

<T extends BoundingType>

限定T为绑定类型BoundingType的子类型。BoundingType可以为类或接口。这里选择关键字extends是因为更接近子类的概念,注意:即使BoundingType为接口,也不能用implements。

一个类型变量可以有多个限定,用&分割:

<T extends BoundingType1 & BoundingType2 & BoundingType3>

又因为java是单继承的,可以实现多个接口,所以限定中至多有一个类,且必须在限定列表中的第一个。

<T extends bClass1 & bInterface1 & bInterface2 ...>

类型擦除

类型擦除是java泛型的一个特性,也是一个理解上的难点。
java代码经过编译后生成的class字节码是在虚拟机中运行的,而虚拟机并没有泛型的概念,里面都是普通类的对象和方法。编译器在编译阶段会将源码中的泛型变量擦除并用相关的类型变量替换,最终在虚拟机上运行。这样的好处就是几乎不需要修改,调用泛型的新代码就可以和旧代码兼容,因为新代码在虚拟机中并没有泛型了,与旧代码一样。

擦除后的类型称为原始类型(raw type), 任何时候定义一个泛型类型,都会自动提供一个相应的原始类型。
原始类型的生成步骤如下:

  1. 删除类型参数
  2. 擦除类型变量,并替换为限定类型,无限定的变量用Ojbect

前面提到的泛型类Pair< T>的原始类型为:

//raw type原始类型
public class Pair {
    private Object min, max;

    public Pair(){
        min = null;
        max = null;
    }

    public Pair(Object min, Object max) {
        this.min = min;
        this.max = max;
    }

    public void setMax(Object max) {
        this.max = max;
    }

    public void setMin(Object min) {
        this.min = min;
    }

    public Object getMax() {
        return max;
    }

    public Object getMin() {
        return min;
    }
}

我们看到,类型参数"< T>“被删除, 原来的T类型的域和方法都被替换为了Object类型的,因为这里的T并没有跟限定符"extends”。由此可知,Pair< String>、Pair< Number>、Pair< Date>的基本类型均为Pair。

如果T为限定类型,比如<T extends bClass1 & bClass2…>,用第一个限定的类型变量来替换。

class Pair<T extends Comparable & Serializable> implements Serializable{
	private T min, max;
    public Pair(T min, T max) {
        this.min = min;
        this.max = max;
    }

    public void setMax(T max) {
        this.max = max;
    }
    
    public T getMax() {
        return max;
    }
}

擦除后的原始类型为:

class Pair<Comparable> implements Serializable{
	private Comparable min, max;
    public Pair(Comparable min, Comparable max) {
        this.min = min;
        this.max = max;
    }

    public void setMax(Comparable max) {
        this.max = max;
    }
    
    public Comparable getMax() {
        return max;
    }
	...
}

假设将两个限定的位置交换一下:

class Pair<T extends Serializable & Comparable >

原始类型将用Serializable替换T,当需要比较两个数的大小时,需要先强制类型转换成Comparable对象。所以为了提高效率,应该将标签接口(即没有方法的接口)放在边界列表的末尾。


虚拟机解析泛型

前面提到,虚拟机中的都是普通类对象,没有泛型。当实际执行泛型语句时,由于类型擦除,编译器会将它们翻译成相关的虚拟机指令。比如:

Pair<String> stringPair = new Pair<>();
...
String maxStr = stringPair.getFirst();

第二条语句被翻译成如下语句在虚拟机中执行:

Object obj = stringPair.getFirst();
String maxStr = (String)obj;

多态与类型擦除

public class DateInterval extends Pair<Date> {
    public DateInterval(Date v1, Date v2){
        super(v1, v2);
    }

    @Override 
    public void setMax(Date value) {
        if(value.compareTo(getMin()) > 0){
            super.setMax(value);
        }
    }
}

DateInterval继承自泛型类Pair< Date>,并覆盖实现了自己的setMax方法,来保证getMax()大于getMin(),而原始的Pair< T> 并没有保证这点。但是这里的覆盖生效了吗?
子类方法覆盖的前提是必须与父类方法具有相同的方法签名(方法名➕参数列表),同时子类方法的访问权限应>=父类方法的访问权限。
当类型擦除后,Pair< Date>变为Pair, 其中的setMax为
setMax(Object value)。同时DateInterval变为:

public class DateInterval extends Pair {
    public DateInterval(Date v1, Date v2){
        super(v1, v2);
    }

    @Override 
    public void setMax(Date value) {
        if(value.compareTo(getMin()) > 0){
            super.setMax(value);
        }
    }
}

DateInterval中的setMax与Pair中的setMax的参数类型不一致,显然子类方法覆盖失败了。这样DateInterval有两个setMax方法:

public void setMax(Date value);//DateInterval类定义
public void setMax(Object value);//继承自Pair

多态,父类引用调用子类方法。我们本意想调用子类中的setMax(Date), 但并没有覆盖成功。这是否意味着泛型和多态调用冲突呢?我们实验一下:

DateInterval interval = new DateInterval(new Date(118, 11, 10),new Date(119, 4, 1));
        Pair<Date> pair = interval;
        Date newDate = new Date(118, 3, 9);
        pair.setMax(newDate);
        System.out.println(String.format("after setMax(newDate), pair: (%s, %s)", pair.getMin(), pair.getMax()));

父类引用Pair< Date> pair引用了子类对象DateInterval,并试图调用子类的setMax方法重设max。

	public void setMax(Date value) {
        if(value.compareTo(getMin()) > 0){
            super.setMax(value);
        }
    }

如果父类的方法被调用,newDate将被设置为max。
如果子类的方法被调用,显然newDate要小于getMin(),所以setMax并没有设置成功,getMax()依然应返回new Date(119, 4, 1)。
看下日志:

11-12 16:13:42.244 2498-2498/com.milanac007.genericdemo I/System.out: after setMax(newDate), pair: (Mon Dec 10 00:00:00 GMT+08:00 2018, Wed May 01 00:00:00 GMT+08:00 2019)

显然多态是正常的。父类引用成功的调用了子类的方法。这是怎么回事呢?这是编译器的功劳。它在DateInterval中生成了一个桥方法(bridge method), 内部调用子类的多态方法。

public void setMax(Object value){
	setMax((Date)value);
}

具体的调用过程如下:
pair 被声明为Pair< Date> 类型,Pair< Date>类有且只有一个setMax:
public void setMax(Object value);
虚拟机用pair实际引用的对象调用上述方法,故实际调用DateInterval的
public void setMax(Object value); 这就是编译器为我们合成的桥方法。

最后我们用反射来验证一下:

try {
            Class<?> cls = Class.forName("com.milanac007.genericdemo.DateInterval");
            Method[] methods = cls.getDeclaredMethods();
            for(Method m:methods) {
                String name = m.getName();
                String returnType = m.getReturnType().getName();
                String modifiers = Modifier.toString(m.getModifiers());
                System.out.print(" ");
                if(modifiers.length() >0) System.out.print(modifiers + " ");
                System.out.print(returnType+ " " + name + "(");

                //print parameter types
                Class[] paramTypes = m.getParameterTypes();
                for(int i=0; i< paramTypes.length; i++) {
                    if(i>0) System.out.print(", ");
                    System.out.print(paramTypes[i].getName());
                }
                System.out.print(");\n");

            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

日志:

...
11-12 16:13:42.244 2498-2498/com.milanac007.genericdemo I/System.out:  public volatile void setMax(java.lang.Object);
11-12 16:13:42.244 2498-2498/com.milanac007.genericdemo I/System.out:  public void setMax(java.util.Date);

小结

  • 虚拟机中没有泛型,只有普通的类和方法;
  • 所有的类型参数都用它们的限定类型替换;
  • 桥方法被合成来保持多态;
  • 为保持类型安全性,必要时插入强制类型转换;

java泛型的限制

  • 不能用基本类型实例化类型参数
  • 不能抛出或捕获泛型类的实例
    既不能抛出也不能捕获泛型类对象,甚至扩展Throwable也不合法。
public class Problem<T> extends Exception{//ERROR can't extend throwable

public <T extends Throwable> void doWork(Class<T> t){
	try{
	
	}catch(T e){//ERROR can't catch type variable

	}
} 
  • 不能实例化类型变量
    比如 new T(…),new T[…], T.class 这样的表达式都是非法的。
    可以调用Class.newInstance方法来构造泛型对象:
Class<T> cl;
cl.newInstance();

Class类本身就是泛型。比如String.class是Class< String>的唯一的实例。

String str = String.class.newInstance();
  • 泛型类的静态上下文中类型变量无效
public class ArrayAlg<T> {
	private static T instance; //ERROR
   	public static T getMiddle(T... array) {//ERROR
        return array[array.length/2];
    }
    //com.milanac007.genericdemo.ArrayAlg.this cannot by referenced from a static context
}

注意,静态方法可以构造成泛型方法:

 	//这里泛型方法中的T与该泛型类的T没关系 
 	public class ArrayAlg<T> {
		public static <T> T getMiddle(T... array) {
	        return array[array.length/2];
	    }
	}  
  • 运行时类型检查只适用于原始类型
if(a instanceof Pair<String>) //ERROR
运行时仅仅能测试a是否为任意类型的一个Pair:
if(a instanceof Pair)//CORRECT

Pair<String> p = (Pair<String>)a;//warning-- can only test a is a Pair

再看getClass的例子:

ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
System.out.println("list1.getClass(): " + list1.getClass());//getClass方法总返回原始类型
System.out.println("list2.getClass(): " + list2.getClass());
if(list1.getClass() == list2.getClass())
  System.out.println("list1.getClass() == list2.getClass()");

运行时,list1和list2对应的原始类型都是ArrayList, 故结果应该为相等。
日志:

11-12 16:35:26.857 4824-4824/com.milanac007.genericdemo I/System.out: list1.getClass(): class java.util.ArrayList
11-12 16:35:26.857 4824-4824/com.milanac007.genericdemo I/System.out: list2.getClass(): class java.util.ArrayList
11-12 16:35:26.857 4824-4824/com.milanac007.genericdemo I/System.out: list1.getClass() == list2.getClass()
  • 不能创建参数化类型的数组
 Pair<String>[] pairs = new Pair<String>[10];//声明Pair<String>[]这个变量是合法的,但不能用new Pair<String>[10]初始化这个变量```

因为类型擦除后,table的类型为Pair[],可以转换为Object[]。
Object[] objarray = table;
objarray会记住它的元素类型,只接收Pair类型的数据,如果试图存储器态类型的元素,就会抛出一个ArrayStoreException:

objarray[0] = "ssss"; //ERROR component type is Pair

下面的代码可以运行,创建了一个Pair[],并赋值给了Object[] array。所以该数组array可以存放Pair型变量,但不能存放Object;注意:pairs[]只能接收Pair< String>型数据。

Pair<String>[] pairs = new Pair[10];
Object[] array = pairs;
//array[0] = new Object();//Caused by: java.lang.ArrayStoreException: java.lang.Object cannot be stored in an array of type com.milanac007.genericdemo.Pair[]
array[0] = new Pair<Double>(2.2, 3.4);//CORRECT
array[1] = new Pair<Integer>(1, 2);//CORRECT
pairs[0] = new Pair<Double>(2.2, 3.4);//ERROR:pairs数组只接收Pair<String>

可以借助通配符和强制类型转换,来间接创建泛型数组,但不建议这么做,ArrayList是更好的选择:

Pair<String>[] table = (Pair<String>[])new Pair<?>[10];
table[0] = new Pair<String >("mike", "lucy");
System.out.println(String.format("table[0].getMin(): %s, table[0].getMax(): %s", table[0].getMin(), table[0].getMax()));

log:

11-13 18:04:17.280 17971-17971/com.milanac007.genericdemo I/System.out: table[0].getMin(): mike, table[0].getMax(): lucy

ArrayList可以用来存放Pair< T>:

ArrayList<Pair<String>> arrayList = new ArrayList<>();
arrayList.add(new Pair<String>("1", "2"));
System.out.println("arrayList.get(0): ("+ arrayList.get(0).getMin() + ", " + arrayList.get(0).getMax() + ")");

log:

11-13 18:04:17.280 17971-17971/com.milanac007.genericdemo I/System.out: arrayList.get(0): (1, 2)

  • 注意类型擦除引发的冲突
public class Pair<T> {
	public boolean equals(T value){
		return min.equals(value) && max.equals(value);
	}
}

用String实例化:Pari< String>, 类型擦除后,变为:

public class Pair {
	public boolean equals(Object value){
		....
	}
}

同时,Pair也从基类Object中继承了equals:

public boolean equals(Object value){...}

两个方法的方法签名和返回类型都相同,编译器报错, 大致意思是这两个方法有相同的类型擦除,谁也不能覆盖谁。这要求我们在定义方法时,应注意擦除后的原始类型方法不能与已有的方法冲突。

这里重命名Pair< T>中的equals方法即可。

泛型类型的继承规则

public class Person{

}

public class Student extends Person{

}

Student是Person的子类,但是对于泛型,Pair< Student>与Pair< Person>之间并不是继承的关系,不论S与T有什么联系, 通常,Pair < S> 和 Pair< T>并没有什么关系。

假设允许Pair< Student>转换成Pair< Person>:

Pair<Student> studentPair = new Pair<>(student1, student2);
Pair<Person> personPair = studentPair;
personPair.setMin(person1);

最后一句是合法的,但这将导致Person和Student组成一对,对于Pair studentPair而言是不合法的。所以Pair< Student>不能转换成Pair< Person>。

注意点

  1. 注意泛型与数组的区别
Student[] students = {stu1, stu2};
Person[] persons = students;//合法
persons[0] = new Person();//ERROR

可以将Student[]数组赋给Person[],这时persons和students都引用了同一份Student型数组。因为数组带有类型保护,之后给该数组添加一个非Student的数据,虚拟机会抛出ArrayStoreException。

  1. 参数化类型永远可以转换为原始类型:
Pair<Student> studentPairs = new Pair<>(stu1, stu2);
Pair pair = studentPairs;//OK
  1. ArrayList< Student>可以转换为List< Student>, 但是ArrayList< Student>与ArrayList< Person>、List< Person>没有关系。

通配符

Pair< Student>并不是Pair< Person>的子类,那么Pair< Student>是谁的的子类呢?答案是Pair<? extends Person>。这就是通配符,它主要作用是限定类型参数的范围,毕竟实际中不一定只是想表达没有范围限制的"任意类型"。

子类型限定

Pair< Person>
Pair< ? extends Person>
Pair< Student>
Pair(raw)

上图可知,Pair< Person>和Pair< Student>均为Pair< ? extends Person>的子类,但它们二者并没有关系。

考虑以下代码:

Pair<Student> studentPair = new Pair<>{stu1, stu2};
Pair<? extends Person> personPair = studentPair;
Person person = personPair.getMin();//CORRECT
personPair.setMin(new Person()); //ERROR

Pair<? extends Person> 中的get/set方法可能如下:

? extends Person getMin();
void setMin(? extends Person);

? extends Person表示Person的子类型,所以将getMin()的返回值赋给一个Person引用是合法的。
但是,setMin的参数为Person的子类,并没有确定是具体的类型。studentPair的类型为Student,personPair.setMin(new Person())将导致Student和Person组成一个集合,这是不对的。

<? extends T> 称为子类型限定,它的访问器方法(get)是安全的,更改器方法(set)不安全。

超类型限定

与子类型限定想对应的是超类型限定,
?super Student 它限制为Student的所有超类型,如Person、Object等。
超类型限定可以为方法提供参数,但不能有返回值。
例如 Pair<? super Student>有方法:

void setMin(? super Student);
? super Student getMin();

编译器不知道setMin的确切类型,但任意的Student对象(Student及它的子类型PrimaryStudent、CollegeStudent)都可以调用它(因为对于任意的确定类型,它都是Student的超类) ,但是不能用Person对象调用。

对于getMin方法,它的返回值时Student的一个超类,但并不确定,只能赋给一个Object。

带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取

超类型限定的一种应用场景,比如下面这个获取数组中的最大最小值:

	public static <T extends Comparable> Pair<T> getMinMax(T... a) {
        if(a == null || a.length == 0)
            return null;

        T min = a[0];
        T max = a[0];

        for(int i=1; i< a.length; i++) {
            if(min.compareTo(a[i])>0){
                min = a[i];
            }

            if(max.compareTo(a[i]) < 0) {
                max = a[i];
            }
        }
        return new Pair<>(min, max);
    }

因为用到了compareTo方法比较大小,所以这里的类型参数需是Comparable的子类。这是没有问题的。
更细化的说,Comparable也是一个泛型,改造一下:

public static <T extends Comparable<T>> Pair<T> getMinMax(T... a) {
...
}

调用:

Integer[] ints = {102, 4, 54, -1};
Pair<Integer> minMax =  ArrayAlg.getMinMax(ints);
System.out.println(String.format("ArrayAlg.getMinMax(ints): (%d, %d)", minMax.getMin(), minMax.getMax()));

GregorianCalendar[] calendars = {
                new GregorianCalendar(1990, Calendar.DECEMBER, 22),
                new GregorianCalendar(1996, Calendar.MARCH, 8),
                new GregorianCalendar(2000, Calendar.MAY, 1),
                new GregorianCalendar(2019, Calendar.AUGUST, 18)
        };
        Pair<GregorianCalendar> calendarMinMax =  ArrayAlg.getMinMax(calendars);
        System.out.println(String.format("ArrayAlg.getMinMax(calendars): (%s, %s)", calendarMinMax.getMin().getTime(), calendarMinMax.getMax().getTime()));

编译报错:

Error:(102, 59) 错误: 对于getMinMax(GregorianCalendar[]), 找不到合适的方法
方法 ArrayAlg.<T>getMinMax(T...)不适用
(推断类型不符合上限
推断: GregorianCalendar[]
上限: Comparable<GregorianCalendar[]>)
其中, T是类型变量:
T扩展已在方法 <T>getMinMax(T...)中声明的Comparable<T>

来看类型限定,<T extends Comparable>
Integer 实现了 Comparable< Integer>接口,没有问题;
GregorianCalendar继承自Calendar,而Calendar实现了Comparable< Calendar>,所以GregorianCalendar并没有实现Comparable< GregorianCalendar>,而实现的是Comparable。
正确的写法应为:

public static <T extends Comparable<? super T>> Pair<T> getMinMax(T... a) {
...
}

类型参数表示 任意类型T,T的超类实现了Comparable接口

无限定通配符

尖括号中添加问号“<?>” 代表无限定通配符。在反射中已经用到了无限定通配符。

Class<?> cls = Class.forName("com.milanac007.genericdemo.DateInterval");

Pair<?>有如下方法:

? getMin();
void setMin(?);

get方法的返回值只能赋给Object,不能调用set方法,甚至不能用Object调用,除了NULL:

Object obj = getMin();//CORRECT
setMin(new Object());//ERROR
setMin(null);//CORRECT

通常<?>用来泛指任意一种确定的类型,这省去了各类具体的表示方法。下面的代码用来测试一个pair是否包含一个null引用,不需要实际指明类型,也没必要:

public static boolean hasNulls(Pair<?> p){
	return p.getMin() == null || p.getMax() == null;
}

但是,通配符并不是类型变量,不能声明一种该类型的变量:

? t = p.getMin();//ERROR

在交换数据的时候须临时保存第一个元素,这里可以使用泛型方法实现:

public static <T> void swapHelper(Pair<T> p){
	T t = p.getMin();
	p.setMin(p.getMax());
	p.setMax(t);
}

然后:

public static void swap(Pair<?> p){
	swapHelper(p);
}

通配符泛指任意一种确定的类型,当然可以作为实参传递给泛型方法。
这种情况下,swapHelper方法的参数T捕获了通配符。

编译器必须能确信通配符表达的是单个、确定的类型时,通配符捕获才合法。比如,ArrayList<Pair< T>>中的T永远不能捕获ArrayList<Pair<?>>中的通配符。后者可以保存两个不同类型的Pair。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值