Java 陷阱

Java 陷阱

1、       Java默认访问权限

在Java中,显示的访问权限修饰符有private、protected、public,若在在定义类,属性,方法时没有显示添加访问权限修饰符,则默认的为package,或称friendly。

在Java中,若子类重写父类的方法,要求子类的访问权限不能低于父类的访问权限,若父类为public,则子类只能为public;若父类为protected,则子类可以为protected或public。

2、       类型提升

Java为强类型语言,当一个算术表达式中包含多个基本类型的值时,整个算术表达式的数据类型将发生自动提升,提升规则如下:

l  byte、short、char提升为int;

l  这个表达式的计算结果类型提升为最高等级的操作数类型,如下


short age=8;

age=age+1;

上面的代码将会报错,由于age+1的结果为int类型(自动类型提升),age为short类型,所以将会报错,需要强制类型转换。

在Java中,long占8个字节,short占4个字节,为什么long类型的可以转换为float类型的?

long count=12;

float temp=count+1;

在Java中,long占8个字节,short占4个字节,为什么long类型的可以转换为float类型的?

因为long为整形,精确到个位,虽然占8个字节,但表示数的范围并没有32位的float所表示的范围大。

3、       switch

在Java中,switch子句的中变量可以为byte、short、char、int、Enum;但在JavaSE8中,switch中支持String类型。

4、       数组

在Java中,数组初始化有两种方式,分为:

l  静态初始化

l  动态初始化

静态初始化是指在定义数组时,显示指定数组元素,如下

int[] arr={1,2,3,4,5,6};

动态初始化如下:

   int[] arr=new int[8];

    for (inti = 0; i < arr.length; i++)

    {

       arr[i]=i;

}

    在Java中数组为引用类型,所以不管数组元素是基本数据类型还是引用类型,都是在堆上分配内存空间的。对于基本数据类型的数组,若在创建数组时没有初始化,则会以“零值”来初始化数组中的所有元素;而引用类型的元素默认为null,所以对于引用类型的数组元素还要单独为每个数组元素赋值,如下图所示


定义数组(未初始化)

 

   数组初始化

在Java中其实数组元素就是变量,如基本数据类型数组,数组元素就是基本数据类型的变量,对于引用类型的数组,数组元素为引用类型的变量。同样,在Java中n维数组元素实质上n-1维数组的变量,如int[][] arrs;arrs中的每一个元素为int[]的变量。

5、       语法糖

语法糖是指在计算机语言中添加的某种语法,这种语法对于语言的功能并没有影响,但是更方便程序员使用,所以语法糖只是编译器的行为。

在Java中常用的语法糖包括自动装箱、拆箱,变长参数,泛型擦除等,虚拟机运行时不支持这些语法,它们会在编译阶段被还原为基础语法结构。

5.1、装箱、插箱

在Java中自动装箱、拆箱语法会在编译后转换为对应的包装和还原方法,如下

  Integer age=10;

 int temp=age;

 编译后的字节码如下:

  bipush        10

  invokestatic #16              //Integer.valueOf;

  astore_1     

  aload_1      

  invokevirtual #22             // Integer.intValue

5.2、For each

在java中数组和容器类型元素可以使用foreach语法来循环遍历,则在编译后还原为对应的迭代器实现,这也就是为何需要被循环遍历的类,需要实现Iterable接口的原因。

5.3、变长参数

在java中,可以在方法的最后面加上变长参数,其实质上,这些变长参数在编译后会转变为数组。

 

5.4、泛型类型擦除

Java中最有迷惑性的语法糖也许就是泛型了。Java中,泛型只会在源码中存在,在编译时,会将这些泛型信息擦除,并在相应的位置添加强制类型转换的代码,基于类型擦除的实现泛型被称为伪泛型。如下所示

public class Test

{

   public void  list(List<Integer>list)

   {

    return null;  

   }

  

   public void list(List<String> list)

   {

     return null;

   }  

}

上面的代码,重载了list方法,但编译器会报错,并不能编译通过。原因在于方法参数类型一致,List<Integer>list,List<String>list在编译时类型擦除后,都为Listlist,二者方法参数相同,故不能够重载。

6、       异常

在Java中,采用try-catch-finally语句块进行异常捕捉和处理时,Java虚拟机向我们保证finally语句块总会执行,事实真的如此吗?

当我们在try、catch语句块包含return语句时,finally语句块还会执行吗?当try-catch、finally语句都包含return语句时,返回值到底是什么?当try、catch语句块包含System.exit(0)语句时,finally语句块还会执行吗?

6.1、return

情形1

public class Main

{

   public static void main(String[] args)

  {

    System.out.println("retuen age="+getAge());

  }

  

  public static int getAge()

  {

    int age=10;

    try

    {

      age=12;

      returnage;

    }

    catch (Exceptione)

    {

      age=15;

    }

    finally

    {

       age=18;

       System.out.println("finally...,age="+age);

    }

    returnage;

  }

 

}

输出结果如下:

finally...,age=18

retuen age=12

 

情形2

 

public class Main

{

   public static void main(String[] args)

  {

    System.out.println("retuen age="+getAge());

  }

  

  public static int getAge()

  {

    int age=10;

    try

    {

      age=12;

      returnage;

    }

    catch (Exceptione)

    {

      age=15;

    }

    finally

    {

       age=18;

       System.out.println("finally...,age="+age);

       returnage;

    }

  }

 

}

输出结果如下:

finally...,age=18

retuen age=18

 

当Java程序执行try块,catch块遇到了return语句,return语句并不会导致该方法立即结束。而是去寻找该方法中是否包含了finally块,如果没有finally块,方法终止,返回相应的返回值。如果有finally块,系统立即开始执行finally块,只有当finally块执行完毕后,系统才会再次跳回return语句处结束方法。如果finally块中有return语句,则finally块已经结束了方法,系统不会再跳回去执行try块或catch块中的任何代码。

若是在try,catch中又抛出其他异常,也即遇到throw语句,则执行流程与return语句一致。

总的来说,即使try块或catch块中有return(或throw)语句,finall语句块也会被执行。若finally中有return语句,则执行finally块中的return语句;若finally块中没有return语句,则返回try块或catch块中的返回值

6.2、exit

public class Main

{

   public static void main(String[] args)

  {

    System.out.println("retuen age="+getAge());

  }

  

  public static int getAge()

  {

    int age=10;

    try

    {

      age=12;

      System.exit(0);

    }

    catch (Exceptione)

    {

      age=15;

    }

    finally

    {

       age=18;

       System.out.println("finally...,age="+age);

    }

    return age;

  }

 

}

若在程序中遇到System.exit(0)语句将使虚拟机停止工作,finally语句并不能被执行。但是,当遇到System.exit(0)语句使虚拟机停止工作前,虚拟机会在退出前执行清理工作,会执行系统中注册的所有关闭钩子,如下

public class Main

{

   public static void main(String[] args)

  {

    System.out.println("retuen age="+getAge());

  }

  

  public static int getAge()

  {

    int age=10;

    //为系统注册关闭钩子

    Runtime.getRuntime().addShutdownHook(newThread(new Runnable()

    {

     

      @Override

      publicvoid run()

      {

        System.out.println("系统关闭钩子被执行...");  

      }

    }));

    

    try

    {

      age=12;

      System.exit(0);

    }

    catch (Exceptione)

    {

      age=15;

    }

    finally

    {

       age=18;

       System.out.println("finally...,age="+age);

    }

    return age;

  }

}

综上所述,当系统显示调用system.exit来退出程序时,若有可能未释放的资源,需要注册系统关闭钩子来正确释放资源。

6.3、正确关闭资源

如何在finally语句块中关闭资源呢?

  public void release()

  {

    ObjectInputStream in=null;

    ObjectOutputStream out=null;

    

    try

   {

        File file=new File("/Users/ssl/obj.data");

        in=new ObjectInputStream(new FileInputStream(file));

        out=new ObjectOutputStream(new FileOutputStream(file));

        //...

   }

   catch (Exceptione)

   {

     

   }

   finally

   {

      if (out!=null)

      {

         try

         {

            out.close();

         }

         catch (IOExceptione)

         {

           

         }

      }

      if (in!=null)

      {

         try

         {

            in.close();

         }

         catch (IOExceptione)

         {

           

         }

      }

   }

}

需要注意的是在关闭资源时,可能也会有异常,所以在关闭资源时也要显示的捕获异常,这样做的好处是:若一个方法内打开多个物理资源,不能因为一个资源在关闭时抛出异常而影响其他资源的释放

6.4、继承异常

在Java中规定,子类重写父类的方法时,不能抛出比父类方法类型更多,范围更大的异常,也即子类只能抛出父类抛出异常的子类

若子类实现多个接口,而且接口中方法声明都相同,则子类实现方法时,只能抛出多个接口抛出异常的交集,如下

//接口A

public interface InterfaceA

{

   void sayHi()throws IOException;

}

//接口B

public interface InterfaceB

{

    void sayHi()throws ClassNotFoundException;

}

//实现类

public class Implement  implements InterfaceA,InterfaceB

{

   @Override

   public void sayHi()

   {

      System.out.println("hi...");

   }

}

public class Main

{

   public static void main(String[] args) throws IOException,ClassNotFoundException

   {

 

       Implement implement=new Implement();

       InterfaceA interfaceA=implement;

       InterfaceB interfaceB=implement;

       interfaceA.sayHi();

       interfaceB.sayHi();

   }

}

 

 

 

7、       类与对象

在Java中,可以通过new关键字来创建某一个类的实例,那么JVM通过那些信息来创建类的实例,这些信息存储在什么地方?为了描述类的信息,当一个类被JVM加载后,就会在JVM中生成一个Class实例,以该实例作为操作类的入口,而且同一个JVM内一个类只有一个Class实例,所以本质上类也是一个实例

理解了类与对象的关系后,我们需要了解类变量(static修饰的变量是类变量,属于类本身)与实例变量的初始化过程,以及构造函数的作用到底是什么?

7.1、类构造函数

在.java文件中,我们定义的构造函数严格来说是对象构造函数,只有创建该类的实例时才会调用对象构造函数。那么什么是类构造函数呢?

我们知道,一个类对应一个Class对象。当一个类被JVM加载后,便会生成一个Class对象,其实类构造函数就是用来初始化Class对象的,那么类构造函数是如何生成的,在.java 文件中又没有定义?

我们知道类变量有两种初始化方式:

l  定义类变量时指定初始值;

l  在静态代码块中,给静态变量赋值;

其实类构造函数就是就是根据上面两种初始化类变量的代码由JVM自动生成类构造函数,类构造函数中代码的顺序与.java文件中初始化类变量的顺序一致,如下

//源文件

public class Base

{

   static

   {

     count=4;

   }

   static int count=3;

}

 

System.out.println(Base.count);

运行结果

3

此外JVM保证在执行类构造函数前一定会先执行父类的类构造函数;而且,JVM会保证类构造函数执行过程中的线程加锁和同步,这也就是在实现单例时直接为类变量指定值的原因,如下

//

public class Base

{

   static

   {

     count=4;

     System.out.println("base static....");

   }

   static int count=3;

  

}

//

public class Sub extends Base

{

   static int  age;

   static

   {

     System.out.println("sub static.....");

   }

}

//测试代码

public static void main(String[] args)

{

     

    System.out.println(Sub.age);

}

输出结果:

base static....

sub static.....

0

在此,我们必须清楚不管是类构造器还是对象构造器作用都是执行初始化,在执行初始化操作之前,对象的内存已分配完毕,并置0值(数值类型为0,布尔值为false,引用类型为null)。其实Java和大多数语言一样,都是采用的两阶段建造对象技术。在第一阶段,分配内存空间,并置0;第二阶段才执行用户的初始化代码,也即执行类构造函数或对象构造函数。如下

//源文件

public class Base

{

    int count=5;

}

//字节码

public class base.Base

{

  int count;

  public base.Base();

    Code:

       0: aload_0      

       1: invokespecial #10    //Method "<init>":()

       4: aload_0      

       5: iconst_5     

       6: putfield      #12                 // Field count:I

       9: return       

}

上面的代码中,虽然我们在定义count时,指定了初始值,但实际上初始化的代码移动到了构造函数中。

 

7.2、对象构造函数

类构造函数是用来初始化Class实例的,而对象构造函数是用来初始化某一个类的实例。我们知道为实例属性指定初始值有三种方式:

l  在定义实例属性时,赋值;

l  在非静态代码块中,赋值;

l  在构造函数中,赋值;

其实,在定义实例属性时赋值或在非静态代码块中赋值这些操作最终都会被转移到构造函数中,而且会被转移到构造函数头部,也即构造函数的赋值操作晚于前两种方式,而前两种赋值的先后与在.java源文件中的顺序一致,如下

//源文件

public class Cat

{

   public  Cat()

   {

     weight=2.6f;

   }

   {

     weight=2.0f;

   }

  

   float weight=2.3f;

  

}

//字节码

public classbase.Cat

 {

  float weight;

 

  public base.Cat();

    Code:

       0: aload_0      

       1: invokespecial #10                 // Method "<init>":()

       4: aload_0      

       5: fconst_2     

       6: putfield      #12                 // Field weight:F

       9: aload_0      

      10: ldc          #14                 // float 2.3f

      12: putfield      #12                 // Field weight:F

      15: aload_0      

      16: ldc          #15                 // float 2.6f

      18: putfield      #12                 // Field weight:F

      21: return       

}

总结,当实例化一个子类对象时,会执行子类的类构造函数,但JVM保证父类类构造函数先于子类类构造函数执行,所以先执行父类类构造函数,再执行子类类构造函数。之后再执行子类的构造函数,若子类构造函数中没有显式调用父类的构造函数,JVM会调用父类的默认构造函数,之后才会执行子类的构造函数。整体的顺序为:父类类构造函数->子类类构造函数->父类构造函数->子类构造函数。

7.3、继承属性

子类继承父类的属性时,会在子类对象中保存所有父类的属性,包括私有的和公有的。若父类和子类中有相同的属性,在子类对象中也会保存两份属性。如下

//

public class Base

{

    int count=5;

}

//

public class Sub extends Base

{

   int count=10;

   void showBase()

   {

     System.out.println("base count="+super.count);

   }

   void showSub()

   {

    System.out.println("sub count="+count);  

   }

}

//

public static void main(String[] args)

   {

     

      Sub sub=new Sub();

      sub.showBase();

      sub.showSub();

   }

//输出结果

base count=5

sub count=10

从上面的代码中,可以看出子类对象确实保存了父类对象的属性,那么子类是如何继承父类的属性呢,是简单的把父类的属性拷贝至子类中,还是其他方式?

public classbase.Base

{

  int count;

 

  public base.Base();

    Code:

       0: aload_0      

       1: invokespecial #10    //Method Object."<init>":()V

       4: aload_0      

       5: iconst_5     

       6: putfield      #12    // Field count:I

       9: return       

}

 

public classbase.Sub extends base.Base

{

  int count;

 

  public base.Sub();

    Code:

       0: aload_0      

       1: invokespecial #10     // Methodbase/Base."<init>":()V

       4: aload_0      

       5: bipush        10

       7: putfield      #12     // Field count:I

      10: return       

}

从字节码文件中,我们看出子类并没有简单的将父类中的属性拷贝到子类中,而是各自属性都在各自类中,只不过在实例化子类时,会在子类对象分配的内存空间中包含所有父类的属性

 

7.4、继承方法

继承方法与继承属性有什么区别吗?

class Base

{

    public void show()

    {

       System.out.println("show...");

    }

}

public class Sub extends Base

{

  

}

//字节码

class base.Base

{

  base.Base();

    Code:

       0: aload_0      

       1: invokespecial #8     // Method Object."<init>":()V

       4: return       

 

  public void show();

    Code:

       0: getstatic     #15                 // Fieldjava/lang/System.out:Ljava/io/PrintStream;

       3: ldc           #21                 // String show...

       5:invokevirtual #23                 //Method java/io/PrintStream.println:(Ljava/lang/String;)V

       8: return       

}

public classbase.Sub extends base.Base

{

  public base.Sub();

    Code:

       0: aload_0      

       1: invokespecial #8      //Method base/Base."<init>":()V

       4: return       

 

  public void show();

    Code:

       0: aload_0      

       1: invokespecial#15    // Method base/Base.show:()V

       4: return       

}

 

从上面的字节码可以看出,编译器会将子类中的方法转移到子类,若子类中有一样的方法,则父类中的方法不会转移到子类中。

注意,父类不使用public修饰,而子类使用public,才可以看到编译器将父类中的方法转移到子类中。

由于继承方法时,对于public方法子类可以覆盖父类中的方法,对于其他private,protected或package方法,子类并不能覆盖;若子类中有同样签名的方法,不过是子类中的方法而已。而继承属性时,父类中的所有属性都会在子类中存储,所以这就导致调用方法和访问属性时的行为并不一致。总结来说访问属性时,依据的是编译期对象的类型(字面类型);而调用方法时,依据的是对象的实际类型,如下

//基类

 

public class Base

{

   private int count=10;

   public Base()

   {

      count=2;

    show();

   }

    public void show()

    {

       System.out.println("count="+count);

    }

}

//子类

public class Sub extends Base

{

   private int count=20;

   public void show()

   {

      System.out.println("count="+count);

   } 

}

//测试

public static void main(String[] args)

{

   new Sub();    

}

//输出结果

count=0

 

对于上面的代码,是不是对输出结果,心存疑虑呢?我们先来看看上面代码的一个执行过程。

new Sub()将会调用Sub的默认构造函数来初始化实例,但Sub继承自Base,所以JVM先执行Base的默认构造函数,

public Base()

{

      count=2;

    show();

}

count=2,实质是this.count=2,那么此处的this到底是Base还是Sub,实质上在运行时this指向Sub实例,但this.count=2是处于Base的构造函数内,所以在编译期this为Base类型,那么count=2,实际上为Base的实例count属性赋值为2,所以父类的count值为2。

show(),实质上this.show(),由于方法调用是运行时实际对象类型的方法,所以这里会执行Sub实例的show方法,Sub实例的show方法如下

public void show()

{

     System.out.println("count="+count);

}

这里输出的是Sub实例的count属性,而此时count属性值只是在分配内存时的0值,Sub的构造函数内的代码还未执行,所以程序将输出0值。当show方法执行完毕后,才执行Sub的构造函数内的代码。若想弄明白具体的执行过程,可以在Base和Sub的构造函数中添加输出语句,如下

public class Base

{

   private int count=10;

   public Base()

   {

    count=2;

    System.out.println("base constructor count="+count);

    show();

   }

    public void show()

    {

       System.out.println("count="+count);

    }

}

public class Sub extends Base

{

   private int count=20;

   public Sub()

   {

     System.out.println("sub constructor count="+count); 

   }

  

   public void show()

   {

      System.out.println("count="+count);

   } 

}

//测试

public static void main(String[] args)

{

   new Sub();    

}

//输出结果

base constructor count=2

count=0

sub constructor count=20

7.5、编译期绑定和运行时绑定

在Java中,多态行为是依靠运行时绑定才实现的,也即在运行时才确定对象的实际类型。但在Java中是不是所有的类型都是在运行时才确定呢?

答案是否定的,在Java中存在一些“编译期可知,运行期不可变”的类型,所以对于这些类型将在编译期完成绑定(根据根据声明的类型来绑定),具体有以下几种情况:

l  静态方法方法

l  私有方法

l  重载方法,在Java中重载的方法是在编译期间绑定的;而重写是多态行为,是在运行期绑定的;

l  实例属性,由于子类也保存了父类中的所有属性,所以在访问属性时,依据的是编译期类型,如下

public class Base

{

  int count=10;

}

   public class Sub extends Base

   {

     int count=20;

  }

   //测试

   public static void main(String[] args)

   {

     

      Sub sub=new Sub();

      Base base=sub;

     

      System.out.println(sub.count);

      System.out.println(base.count);

   }

//输出结果

20

10

7.6、final

在Java中final可以修饰类、方法、属性,意味为不可变,具体如下:

l  修饰类时,该类不能被继承;

l  修饰方法时,该方法不能被重写;

l  修饰属性时,为不可变属性,对于基本类型的数据,这些值不能改变;对于引用类型,引用不能再指定其他的实例,而指向的实例本身是可以改变的;

对于final修饰的属性,并且指定了初始值,如final int count=5;就可以在编译期确定属性的类型,那么这个final属性已不再是变量,而是相当于一个直接常量

 

8、       内部类

在Java中,内部类分为非静态内部类、静态内部类和局部内部类等。

8.1、非静态内部类

在Java中,非静态内部类,可以访问外部类的所有的属性和方法,那么为什么内部类可以访问外部类的属性和方法呢?

内部类持有外部类对象实例的引用,如下所示

public class Person

{

    class Address

    {

       private Stringinfo;

        public Address()

      {

       

      }

       

        public Address(Stringinfo)

        {

       

        }

    }

}

内部类对应的字节码如下

classbase.Person$Address

{

  final base.Person this$0;

  public base.Person$Address(base.Person);

    Code:

       0: aload_0      

       1: aload_1      

       2: putfield      #12         // Field this$0:Lbase/Person;

       5: aload_0      

       6: invokespecial #14    //Methodjava/lang/Object."<init>":()V

       9: return       

 

  public base.Person$Address(base.Person, java.lang.String);

    Code:

       0: aload_0      

       1: aload_1      

       2: putfield      #12       // Field this$0:Lbase/Person;

       5: aload_0      

       6: invokespecial #14                 // Methodjava/lang/Object."<init>":()V

       9: return       

}

从字节码可以看出非静态内部类中持有外部类的引用,且非静态内部类的构造函数在编译之后会增加外部类引用的参数,以便初始化该内部类。所以,非静态内部类必须依赖外部类的实例而存在,所以导致非静态内部类不能拥有静态属性或方法

8.2、静态内部类

被static修饰的成员,如属性,方法,块代码等属于类本身,而不属于实例,那么对于static修饰的内部类,也是如此,静态内部类属于外部类,而不属于外部类的实例。所以,对于静态内部类不能访问外部类的实例属性或方法,其实对于静态内部类而言,外部类只不过相当于一个包而已

8.3、局部内部类

对于在方法或其他代码块中的内部类,称为局部内部类。在局部内部类中,若要访问外部变量,必须要求外部变量为final的。原因如下,对于普通变量而言,作用域在方法体内,方法执行完毕,局部变量也就不存在了;但是对于内部类而言,可能会产生隐式的闭包,闭包将使得扩大局部变量的作用域,使其脱离方法体后,继续存在。如下

public static void run()

   {

      final int count=10;

      new Thread(new Runnable()

      {

         class Count

         {

            void cutdown()

            {

               inti=count;

               while (i>0)

               {

                  System.out.println("cutdowni="+i);

                  i--;

               }

            }

         }

         @Override

         public void run()

         {

            new Count().cutdown();

         }

      }).start();

   }

由于count局部变量,在局部内部类中仍要被访问,扩大了count变量的作用域。由于可能产生隐式闭包,扩大局部变量的作用域,为了安全起见,Java编译器要求被内部类访问的变量必须使用final修饰符修饰。

9、       内置锁

Java内置锁分为对象锁和类锁,类锁和该类对象的锁并不是一把锁,是不能用于互斥操作的,在Java中若想达到互斥的效果,对临界区必须使用同一把锁进行加锁。如下,实例方法和类方法并并不能互斥。

public class Main

{

   public static void main(String[] args)

   {

     new Thread(new Runnable()

    {

       @Override

       public void run()

       {

         Test.classMethod();  

       }

    }).start();

     

      new Thread(new Runnable()

        {

          @Override

          public void run()

          {

             new Test().instanceMethod();

          }

        }).start();

  

  }

  

  static class Test

  {

    public static synchronized void classMethod()

    {

        for (inti = 0; i < 10;i++)

       {

          System.out.println("class method of loop "+i); 

       }

    }

    

    public synchronized void instanceMethod()

    {

        for (inti = 0; i < 10;i++)

        {

             System.out.println("instance method of loop "+i); 

        }

    }

    

  }

}

输出结果:

class method of loop 0

class method of loop 1

instance method of loop 0

class method of loop 2

class method of loop 3

instance method of loop 1

class method of loop 4

instance method of loop 2

class method of loop 5

instance method of loop 3

instance method of loop 4

class method of loop 6

instance method of loop 5

instance method of loop 6

instance method of loop 7

instance method of loop 8

instance method of loop 9

class method of loop 7

class method of loop 8

class method of loop 9

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值