20201124java基础知识整理

JDK JRE JVM关系

  • JDK是java开发工具包,包含java开发工具(编译工具javac.exe,运行工具java.exe)和JRE
  • JRE是java运行环境,包含JVM和java所需核心类库。只需要JRE就可以运行开发好的java程序
  • JVM是java虚拟机,是虚构出来的计算机,是通过仿真各种实际计算机实现的。java的跨平台就是建立在jvm的基础上,任何平台只要装有针对该平台的虚拟机,就可以执行编译好的字节码文件。这就是“一次编译,多次运行”。
    关系图

java运行原理

  • 通过javac编译我们编写的java文件,生成class文件
  • 通过java执行生成的class文件

基本数据类型

基本数据类型

++运算

a++时,a的值会加1,但是a++还是等于a

	int a = 1;
    int b = a++;
    System.out.println(a);//2
    System.out.println(b);//1

++a时,a的值会加1,此时++a等于a+1

	int a = 1;
	int b = ++a;
	System.out.println(a);//2
	System.out.println(b);//2

类型转换

自动类型转换
  • 小类型转大类型:例如int可自动转为long,但是double不能自动转为float
  • byte–>short–>int–>long–>float–>double
  • char–>int–>long–>float–>double
运算符对基本类型的影响

当使用+、-、*、/、%运算符对基本类型进行运算时,遵循以下原则

  • 不同类型数据运算,小范围转为大范围的数据,再进行运算
  • 如果是byte、short、char在运算时,首先转为int,然后再运算
	System.out.println('a'+1);//98
	System.out.println("a"+1);//a1
char和byte类型
  • 因为字符也是以数字存储,比如a为97,本质上也是数字。所以char可以与int类型相互转换
	System.out.println('a'==97);//true
  • 在0-127范围内,char和byte可以相互几乎没有区别。
  • byte表示-128-127之间的整数,char表示字符
  • char只能表示正整数
赋值运算符
  • s=s+2编译失败,因为s会被提升为int类型,运算后的结果还是int类型,无法赋值给short类型。
  • s+=2; 编译通过,因为+=运算符在给s赋值时,自动完成了强转操作。

switch的穿透效果

        int mingCi = 2;
        switch (mingCi){
            case 1:
                System.out.println("出任武林盟主");//不执行
            case 2:
                System.out.println("出任武当掌门");//执行
            case 3:
                System.out.println("出任峨嵋掌门");//执行
            default:
            	//如果default在2以下执行。
            	//如果default在2以上不执行
                System.out.println("被逐出师门");//执行
        }
		int i = 3;
	       switch(i){
	           default:
	               System.out.println("default");//没有匹配到,执行default
	           case 0:
	               System.out.println("0");//执行
	               break;//如果没有break,会继续往下执行
	           case 1:
	               System.out.println("1");
	           case 2:
	               System.out.println("2");
	       }
  • 穿透效果就是找到需要执行的项,如果没有break,就继续往下执行
  • default不建议省略,且建议写到最后

循环控制break、continue、return

  • break 中断break所在的循环
  • continue 跳过本次循环,执行下次循环
  • return 结束整个方法

方法重载

  • 同一个类中,允许一个以上同名方法。只要他们参数个数或者参数类型不同即可
  • 与public/private 返回值等无关
	//这种是方法重载,虚拟器可以通过参数进行区分,因为传参顺序不同
    public void amethod(int i, String s){ }
    public void amethod(String s, int i){ }

java堆栈内存及垃圾回收

        int[] arr = new int[3];
        int[] brr = arr;
        int[] crr = new int[3];

        arr[0] = 1;
        brr[0] = 2;

		// 证明数组是引用数据类型
        System.out.println(arr);//[I@1540e19d
        System.out.println(brr);//[I@1540e19d
        System.out.println(crr);//[I@677327b6
        System.out.println(arr[0]);//2

        arr=null;
        System.out.println(arr);//null
        System.out.println(brr);//[I@1540e19d
  1. [I@1540e19d表明数组是引用数据类型,brr = arr是直接将arr的地址赋给brr,所以brr修改之后,arr也会随之改变。
    其中[表示数组;I表示int; @表示存放位置;1540e19d表示位置信息
    在这里插入图片描述
    bean对象同理

  2. 变量arr存放于栈,new出来的对象存在于堆。当arr = null时,解除引用关系,堆中的对象等待GC回收,栈中的变量在超出作用域后会被主动释放。上述代码因为还存在brr的引用,所以不会被回收。
    参考链接:https://zhuanlan.zhihu.com/p/25539690

局部变量和成员变量区别

  • 所在位置:成员变量存在于类内部、方法外,局部变量存在于方法内
  • 存放位置:成员变量存放于堆和对象在一起,局部变量存放于栈
  • 默认值:成员变量有默认值,局部变量没有默认值
  • 生命周期:成员变量随着类,局部变量随着方法

构造方法

  • 格式:访问修饰符(private除外)+类名(参数){}
  • 默认无参构造,如果用户手动添加,则不再默认,此时如果没有无参构造,则不能通过无参形式new对象

this关键字

  • this表示当前对象
  • 当局部变量和成员变量属性名相同时,通过this.name表示成员变量,this.name=name;
  • 通过this.fun(args)调用方法,this可以省略。
  • 通过this(参数)的形式,可以调用构造方法

static关键字

  • 内存分为stack segment(栈)、heap segment(堆)、code segment(代码区)、data segment(数据区)
  • 类中的方法存在于code segment(代码区)
  • static的变量和字符串常量存在于data segment(数据区)
  • static修饰的属性和方法也可以通过对象访问,例如:this.静态属性;new Car().静态方法。但是不建议
  • 静态方法和属性生命周期随类,所以静态方法不能使用非静态属性和非静态方法,因此静态方法中不能使用this。
  • static可以修饰内部类,不能修饰普通类,静态方法使用的内部类,必须用static修饰。

代码块、构造代码块、静态代码块

  • 代码块:存在于方法中,仅用于限制变量的作用范围。不常用
    public static void main(String[] args) {
        {
            int a = 10;
        }
        System.out.println(a);//编译报错
    }
  • 构造代码块:类内方法外,每次执行构造方法之前都会执行构造代码块。不常用
public class Car {
    
    {
        System.out.println("构造代码块,每次执行构造方法之前执行");
    }

    public Car(String name) {

    }

    public String brand;
}
  • 静态代码块:类中方法外,只在类加载的时候执行一次

继承

子类实例化过程
  • JVM会读取指定的路径下的Zi.class文件,并加载进内存,并会先加载Zi的父类.
  • 在堆内存开辟空间,分配地址。
  • 调用构造方法初始化,在调用构造方法时首先调用父类方法进行初始化
  • 父类初始化完成之后,再对子类属性进行初始化
  • 初始化完毕后,将地址赋值给变量Zi zi
class Fu {
    Fu() {
        show();  //通过结果可以看出,子父类有同名show方法,调用的是子类show方法。
    }

    void show() {
        System.out.println("fu show");
    }
}

class Zi extends Fu {
    int num = 8;

    Zi() {
    	//super之前不允许有代码
        super();
        //通过super初始化父类内容时,子类的成员变量并未显示初始化。
        //等super()父类初始化完毕后,才进行子类的成员变量显示初始化。
        System.out.println("zi cons run...." + num);
    }

    void show() {
        System.out.println("zi show..." + num);
    }
}

class ExtendsDemo5 {
    public static void main(String[] args) {
        Zi z = new Zi();
        z.show();
    }
}

//执行结果
//zi show...0
//zi cons run....8
//zi show...8
super关键字
  • super可以调用父类方法、父类属性、调用父类构造
  • 当父类的方法子类重写的时候,调用的是子类的方法。如果要调用父类的方法,需要使用super.fun(args)
  • 当父类属性子类重新赋值时,使用的是子类的值。如果要使用父类的属性值,需要使用super.args
方法重写
  • private方法不能重写
  • 构造方法不能重写,不能继承
  • 除了方法体和访问修饰符必须保持一致
  • 子类的访问修饰符权限一定要大于等于父类权限
多态
  • 编译看父类
  • 运行时成员变量看父类,方法看子类

public class Fu {
    String name="fu";
    public void say(){
        System.out.println("fu--function");
    }
}

class Zi extends Fu{
    String name="zi";

    public void say() {
        System.out.println("zi--function");
    }
    public void say2() {
        System.out.println("zi--function2");
    }
}

class Test{
    public static void main(String[] args) {
    	//1.存在继承关系
		//2.子类重写方法
		//3.父类引用指向子类对象
        Fu fu=new Zi();
        //编译看父类,Fu中没有say2方法,所以编译报错
        //fu.say2();
        
        //运行时成员变量结果看父类
        System.out.println(fu.name);//fu
        //运行时方法结果看子类
        fu.say();//zi--function
    }
}

final关键字

  • final修饰类,该类不能被继承
  • final修饰方法,该方法不能被重写
  • final修饰属性,该属性不能被修改,需要初始化
    • 如果是基本数据类型,不能修改“值”
    • 如果是引用数据类型,不能修改“内存地址”

抽象类和接口

抽象类意义
  • 抽象类的意义在于捕捉所有子类的共性,创建子类的模板,提高代码复用性。
  • 模板差异部分通过抽象方法暴露给子类,子类必须重写抽象方法来实现差异性。
接口意义
  • 接口是特殊的抽象类
  • 接口是抽象方法的集合,类似一种契约或规范,实现了该接口就必须重写所有抽象方法
  • 接口内方法:public abstract 返回值类型 fun(Args args)
  • 接口内属性:默认public static final 数据类型 args=“初始化值”
    接口内属性和方法默认格式
两者都能实现多态

抽象类

public abstract class Animal {
    public String name;
    public abstract void say();
}

class Cat extends Animal {
    public void say() {
        System.out.println("miaomiao~~");
    }
}

public static void main(String[] args) {
	//setAnimal(Animal animal){this.animal=animal}
	setAnimal(new Cat());
}

接口

public interface ISay {
   public static final String name = "dog";
   public abstract void run();
}

public class Auto implements ISay{
    public void run() {
        System.out.println("auto run");
    }
}

public static void main(String[] args) {
	//接口不能直接new ISay();编译报错,可以直接new 实现类
	//我们常用的监听模式,是通过匿名内部类实现的,并不是直接创建了一个接口
    ISay iSay=new Auto();
    iSay.run();//auto run
}
抽象类和接口的区别
  • 设计原则:抽象类相当于是给子类设计的模板,包含子类共性功能;接口是给实现类添加的契约和规范
  • 构造方法:抽象类有构造方法(子类要用),但是不能创建对象;接口没有构造方法(安卓常用创建监听其实是匿名内部类,后边会解释)
  • 成员变量:抽象类可以有变量或常量;接口只能有常量
  • 成员方法:抽象类可以有普通方法,也可以有抽象方法;接口只能有抽象方法
public interface ISay {
   //属性只能是常量
   public static final String name = "dog";
   //必须是抽象方法,默认public abstract
   public abstract void run();
}

匿名内部类

  • 匿名内部类,本质上是父类的子类、接口的实现类
  • 匿名内部类有两种格式
    • 继承式匿名内部类
    • 接口式匿名内部类
public class TestJava {
    public static void main(String[] args) {
        //匿名内部类父类方式
        testSay(new Father(){
            public void say() {
                System.out.println("匿名内部类 say");
            }
        });

        //匿名内部类接口方式
        testSpeak(new IFather() {
            public void speak() {
                System.out.println("匿名内部类 speak");
            }
        });
    }

    public static void testSay(Father f){
        f.say();
    }

    public static void testSpeak(IFather f){
        f.speak();
    }

    public static class Father {
        public void say(){
            System.out.println("father say");
        }
    }

    interface IFather{
        void speak();
    }
}

IO流

IO体系包含:文件(File)、字节流、字符流

  • 字节流一次读写8个字节,适用于图片、音视频、文本
  • 字符流一次读写16个字节,适用于文本文件、FileReader和FileWriter不适用于读写Word
  • 字节流没有缓冲区,字符流有8k大小的缓冲区。字节流是硬盘与内存直接交互,
  • 字符流读:从硬盘到缓冲区,到了8k再去内存;写:缓冲区到缓冲区,到了8k写入硬盘
File

File常用方法

        //1.创建file对象,1.txt或者文件夹可以不存在
        File file=new File("d:\\1.txt");
        File file=new File("d:\\a\\b\\c");
        //2.创建file对象对应的文件
        file.createNewFile();
        //3.创建文件夹
        file.mkdirs();
        //4.判断file对应的是文件还是文件夹
        file.isFile();
        file.isDirectory();
字节流

字节流包含输入流(InputStream)、输出流(OutputStream)

FileOutputStream
  • 构造方法
    • 如果文件不存在,默认创建文件,但是不会创建文件夹
    • 第一个参数可以是File或path,第二个参数表示覆盖或追加,默认覆盖
		FileOutputStream fos=new FileOutputStream(new File("c:\\a\\1.txt"),false);
		FileOutputStream fos=new FileOutputStream("c:\\1.txt",false);
  • 功能方法
    • fos.write(int a) 写入的是a对应的ASCII字符
    • fos.write(byte[] b) 写入byte[]
    • fos.write(byte[] b,int offset,int len) 将byte[]从offset开始,长度len
		fos.write(97);//a
		fos.write("hello".getBytes());//hello
		fos.write("world".getBytes(),1,2);//or
FileInputStream

将文件中的数据,读取到内存中

  • 构造方法
    • 不会创建文件,因为是要从该文件中读取数据,所以必须存在。且只有一个参数
	FileInputStream fis=new FileInputStream("C:\\a\\1.txt");
    FileInputStream fis=new FileInputStream(new File("C:\\a\\1.txt"));
  • 功能方法
	public int read();//每次只读取一个字节,返回的是ASCII值。读取a返回的值是97,没有返回-1
	public int read(byte[] b);//读取b.length的数据到byte[]中,返回的是读取数据的长度,没有返回-1
读写示例
	public static void main(String[] args) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        byte[] b = new byte[10];
        try {
        	//创建输入流,读取1.txt内容。1.txt必须存在
            fis = new FileInputStream("d:\\a\\1.txt");
            //创建输出流,将数据写入2.txt。a文件夹必须存在,2.txt可以不存在。
            fos = new FileOutputStream("d:\\a\\2.txt");
            int len = 0;
            //循环将数据读入b,fos将b写入2.txt
            while ((len = fis.read(b)) != -1) {
            	//如果直接fos.write(b)会有偏差,遍历b数组数据如下
            	//-50-46-80-82-60-29-42-48-71-6
				//-93-84-50-46-57-41-80-82-60-72
				//-57-41-50-46-57-41-80-82-60-72
				//可以发现fis读取数据的时候,是在原来数组的基础上写入。
				//所以如果最后一次读取的长度不是b.length的话,最后一次b中的数据,会跟上一次的有重合
				//fos.write(b)会将0123456789a写成0123456789a123456789
                fos.write(b, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fos.close();
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
字符流
FileWriter
  • 构造方法
    • 如果文件不存在,默认创建文件,但是不会创建文件夹
    • 第一个参数可以是File或path,第二个参数表示覆盖或追加,默认覆盖
FileWriter fw = new FileWriter("fw.txt", false);
FileWriter fw = new FileWriter(new File("fw.txt"), false);
  • 功能方法
    • fw.write(int a) 写入的是a对应的ASCII字符
    • fw.write(String s) 写入的是String
    • fw.write(char[] b) 写入char[]
    • fw.write(char[] b,int offset,int len) 将char[]从offset开始,长度len
fw.write(97);//a
fw.write("a");//a
fw.write("hello".toCharArray());//hello
fw.write("world".toCharArray(), 1, 3);//ord
FileReader
  • 构造方法
    • 不会创建文件,因为是要从该文件中读取数据,所以必须存在。且只有一个参数
fr = new FileReader("fw.txt");
fr = new FileReader(new File("fw.txt"));
  • 功能方法
public int read()//一次读一个字符, 返回的是unicode值,读不到数据返回-1
public int read(char[] c)//一次读c.length个字符到c数组,  返回的是数据的长度,读不到数据返回-1
字符流复制文件示例
字符流不能读写多媒体文件,FileReader、FilerWriter读写word会乱码
FileWriter fw = null;
FileReader fr = null;
try {
    fr = new FileReader("fw.txt");
    fw = new FileWriter("d:\\fw.txt");
    int len = 0;
    char[] c = new char[10];
    while ((len = fr.read(c)) != -1) {
        fw.write(c, 0, len);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        fr.close();
        fw.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
缓冲区
  • 如果没有缓冲区,读一次,写一次
  • 如果有了缓冲区,40k的文件,默认8k缓冲区,那么只读写5次。
    • 读:byte[] b=new byte[1024];第一次读的时候,把8k数据塞到输入流的缓冲区,然后每次从输入流缓冲区读取1024,直到读完。再次读的时候,再塞8k
    • 写:byte[] b=new byte[1024];直接写1024到输出流缓冲区,直到写满输出流的缓冲区,下次再写的时候一次性写入硬盘
  • 程序会一直从输入流缓冲区读,读到输出流的缓冲区。所以输入流如果不关闭,不会有写不完的情况,但是输出流如果不关闭或者flush,会有一部分数据在缓冲区没有写。
缓冲流
  • FileInputStream、FileOutputStream、FileReader、FilerWriter是基本流
  • BufferedInputStream 、BufferedOutputStream 、BufferedReader、BufferedWriter是缓冲流,默认8k缓冲区,但是可以自定义大小
  • FileInputStream、FileOutputStream没有缓冲区;FileReader、FilerWriter有8k缓冲区
  • 缓冲流又称高效流,比基本流更快(读写一个5g大小文件,基本字节流146734ms,缓冲字节流128132ms,差18s)
缓冲字节流
try {
	//构造方法第一个参数:一个基本字节流;第二个参数:选填,自定义缓冲区大小,默认8k
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("F:\\Ghost_Win7_X64_CJ_V2018.12.GHO"), 10 * 1024);
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\Ghost_Win7_X64_CJ_V2018.13.GHO"), 10 * 1024);
    byte[] b = new byte[10 * 1024];
    int len = 0;
    //读写方法与基本字节流相同,不再描述
    while ((len = bis.read(b)) != -1) {
        bos.write(b, 0, len);
    }
    bis.close();
    //正常情况下,缓冲区满了就写。
    //如果不执行bos.close(),最后一车数据还在缓冲区,不会写到文件
    bos.close();
} catch (Exception e) {
    e.printStackTrace();
}
缓冲字符流
  • BufferedWriter 有独有方法newLine();在任何平台都可以开启下一行
  • BufferedReader有独有方法readLine()直接读一行,返回值为读取到的数据
  • 建议多媒体文件缓冲字节流速度快,文本文件基本字符流更方便

因为缓冲字符流和基本字符流的区别除了缓冲字符流可以自定义缓冲区大小,就是缓冲字符流有独有方法,但是使用如下所示独有方法因为readLine的原因,没有换行,如果每次write之后,都使用newLine又会多写一行,总之个人还是喜欢基本字符流读写文本。当然如果不介意多一行,缓冲字符流还是推荐的

try {
    BufferedReader br = new BufferedReader(new FileReader("fw.txt"), 8*1024);
    BufferedWriter bw = new BufferedWriter(new FileWriter("writer.txt"), 8*1024);
    String temp = null;
    while ((temp = br.readLine()) != null) {
        bw.write(temp);
        bw.newLine();
    }
    bw.close();
    br.close();
} catch (Exception e) {
    e.printStackTrace();
}
转换流
  • gbk编码一个汉字2个字节,utf-8一个汉字3个字节
  • InputStreamReader将字节转换为字符(也就是读)时解决中文乱码问题
//构造方法可以自己指定编码(要跟文件编码一致)
public InputStreamReader(InputStream in,  String charsetName);
//功能方法
public int read()
public int read(char[] cbuf)
  • OutputStreamWriter将字符转换为字节(也就是写)时解决中文乱码问题
//构造方法可以自己指定编码(要跟文件编码一致)
public OutputStreamWriter(OutputStream out,  String charsetName);
//功能方法
public void write(char[] cbuf)
public void write(char[] cbuf, int offset, int len)
public void write(String str)
IO总结

IO关系图
常用流

  • 4个基本流FileOutputStream、FileInputStream、FileReader、FileReader
  • 4个缓冲流BufferOutputStream、BufferedInputStream、BufferedWriter、BufferedReader
  • 2个转换流
  • 多媒体文件尽量使用BufferOutputStream、BufferedInputStream
  • BufferedWriter、BufferedReader虽然有独有方法,并且可以自定义缓冲区,但是文本文件个人还是倾向FileReader、FileReader

进程和线程

进程
  • 进程就是一个正在运行的程序,一般一个程序一个进程。某些软件采用多进程,一个程序采用多个进程
  • 多进程架构:更快,但是更占用资源
  • 进程与进程之间相互独立,不能共享资源
线程
  • 线程是进程中的一个执行单元
  • 线程与线程之间互相独立,但是可以共享进程中的资源
多线程和CPU之间的关系
  • 多线程可以同时执行多个不同代码
  • 真正的同时是不存在的,CPU是在多个线程之间极快随机的来回切换

有可能方法还没执行完,就已经切换走了

实现多线程的两种方式
  • 继承Thread
  • 实现Runnable(比较麻烦,但是扩展性好。因为java只能单继承,继承了Thread就不能继承其他类)
public class TestThread {
    public static void main(String[] args) {
        ThreadStudent ts = new ThreadStudent();
        Thread t = new Thread(new RunnableStudent());

        ts.setName("继承");
        t.setName("实现");

        ts.start();
        t.start();
    }
}

class ThreadStudent extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}

class RunnableStudent implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
线程安全问题
  • 产生原因:CPU随机切换
  • 产生场景:多个线程操作共同资源
  • 解决方法:线程锁,执行完锁的方法再切换
  • 实现方式:1.同步代码块(建议使用)2.同步方法
public class TestThread {
    //车票数量,共同资源
    private static int num = 10;
    public static void main(String[] args) {
        //两个线程,操作了共同资源
        Thread t1 = new Thread(new SaleWindow());
        Thread t2 = new Thread(new SaleWindow());
//
//        Thread t1 = new Thread(new SaleWindow2());
//        Thread t2 = new Thread(new SaleWindow2());
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }

    static class SaleWindow implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                //锁必须是同一个,如果有两个锁那还是会发生线程安全问题
                //例如t1和t2都new SaleWindow();所以此时如果用this,那就是两把锁
                synchronized (SaleWindow.class) {
                    if (num > 0) {
                        System.out.println(Thread.currentThread().getName() + "卖出" + num);
                        num--;
                    }
                }
            }
        }
    }

   static class SaleWindow2 implements Runnable {
        //车票数量

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                sale();
            }
        }
        //默认锁是this。t1和t2都new SaleWindow2(),此时如果不是static方法,那就是两把锁,还是有线程安全问题
        //如果是静态方法,锁是SaleWindow2.class,一把锁不会出现线程安全
        private static synchronized void sale() {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出" + num);
                num--;
            }
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值