缓冲流、转换流、序列化流、打印流

4 缓冲流

4.1 目标

1、能够使用 BufferedInputStream字节缓冲流读取数据到程序

2、能够使用字节缓冲流写出数据到文件

3、能够明确字符缓冲流的作用和基本用法

4、能够使用缓冲流的特殊功能

4.2 缓冲流概述

原来的字节/字符 输入输出流,每次之恶能读取一个字节/字符,效率比较低,增加缓冲流的目的就是为了给基本的字节/字符流增加一个缓冲区,以此提高字节/字符读取文件的效率。

4.3 字节缓冲流

4.3.1 BufferedInputStream 字节缓冲流

java.io.BufferedInputStream 字节缓冲输入流 extends InputStream字节输出流

继承父类的成员方法:

  • int read() 一次读取一个字节并返回
  • int read(byte[] b) 使用数组缓冲,一次读取多个字节
  • void close() 关闭此输入流并释放与该流关联的所有系统资源

构造方法:

  • BufferedInputStream(InputStream in) 创建一个具有默认缓冲区大小的BufferedInputStream对象
  • BufferedInputStream(InputStream in , int size) 创建具有指定缓冲区大小的BufferedInputStream对象

构造方法参数:

  • InputStream in : 传递字节输入流,可以传递InputStream的任意子对象,比如FileInputStream对象,这样就会给FileInputStream对象增加一个缓冲区
  • int size : 指定缓冲区的大小,不写使用默认值8192 ,private static int DEFAULT_BUFFER_SIZE = 8192; 即8k

使用步骤:

  • 1、创建BufferedInputStream对象,构造方法中传递FileInputStream对象;
  • 2、使用BufferedInputStream 对象中的方法read,以字节的方式读取文件;
  • 3、释放资源

4.3.2 BufferedOutputStream 字节缓冲输出流

java.io.BufferedOutputStream 字节缓冲输出流 extends OutputStream字节输出流

继承父类的成员方法:

  • public void close() 关闭此输出流并释放与此流相关联的任何系统资源;
  • public void flush() 刷新此输出流并强制任何缓冲的输出字节被写出;
  • public void write(byte[] b) 将b.length()字节从指定的字节数组中写入到此输出流;
  • public void write(byte[] b , int off, int len) 从指定的字节数组写入len字节,从偏移量off开始输出到此输出流;
  • public abstract void write(int b) 将指定的字节输出流

构造方法:

  • BufferedOutputStream(OutputStream out) 创建一个新的缓冲输出流,具有默认缓冲区大小
  • BufferedOutputStream(OutputStream out, int size) 创建一个新的缓冲输出流,具有指定缓冲区大小

参数:

  • OutputStream out 传递字节输出流,可以传递OutputStream 的任意子类对象,比如 FileOutputStream对象,这样就可以给FileOutputStream对象增加一个缓冲区
  • int size 指定缓冲区的大小,不写使用默认值

使用步骤:

  • 1、创建BufferedOutputStream对象,构造方法中传递FileOutputStream对象
  • 2、使用BufferedOutputStream对象中的方法write,把数据写入到内存缓冲区中
  • 3、使用BufferedOutputStream对象中的方法flush,把内存缓冲区中数据刷新到文件
  • 4、释放资源(会先调用flush方法刷新数据到文件)

4.4 字符缓冲流

4.4.1 BufferedReader 字符缓冲输入流

java.io.BufferedReader:字符缓冲输入流 extends Reader字符输入流

继承父类的成员方法:

  • int read() 读取单个字符
  • int read(char[] cdbuf) 将字符读入数组
  • void close() 关闭流并释放与之有关的所有资源

构造方法:

  • BufferedReader(Reader in) 创建一个使用默认大小缓冲区的缓冲字符输入流
  • BufferedReader(Reader in, int size) 创建一个指定大小缓冲区的缓冲字符输入流

参数:

  • Reader in : 传递字符输入流,可以传递Reader 的任意子类对象,比如FileReader,缓冲流就会给 FileReader 增加一个缓冲区,以此提高FileReader读取文件的效率
  • int size : 指定缓冲区的大小,也就是数组的长度,不指定则使用默认值

特有成员方法:

  • String readLine() 读取一个文本行,一次可以读取一行数据,通过这些字符之一来判定某行是否终止: 换行\n 回车\r 或者回车加换行 \r\n ,返回包含该行内容的字符串,不包含任何终止符,如果已经到了流的末尾,则返回null

使用步骤:

  • 1、创建BufferedReader对象,构造方法中传递FileReader对象
  • 2、使用BufferedReader对象中的方法read|readLine,以字符的方式读取文件
  • 3、释放资源

4.4.2 BufferedWriter 字符缓冲输出流

java.io.BufferedWriter:字符缓冲输出流 extends Writer:字符输出流

继承父类的成员方法:

  • public abstract void close() :关闭此输出流并释放与此流相关联的任何系统资源
  • public abstract void flush() :刷新此输出流并强制任何缓冲的输出字符被写出
  • public void write(int c) :写出一个字符
  • public void write(char[] cbuf) :将 b.length字符从指定的字符数组写出此输出流
  • public abstract void write(char[] b, int off, int len) :从指定的字符数组写出 len字符,从偏移量 off开始输出到此输出流
  • public void write(String str) :写出一个字符串
  • public void write(String str, int off, int len) 写入字符串的某一部分

构造方法:

  • BufferedWriter(Writer out) 创建一个使用默认大小输出缓冲区的缓冲字符输出流
  • BufferedWriter(Writer out, int sz) 创建一个使用给定大小输出缓冲区的新缓冲字符输出流

参数:

  • Writer out:传递字符输出流,可以传递Writer的任意的子类对象,我们可以FileWriter,缓冲流就会给FileWriter增加一个缓冲区,以此提高FileWriter写入文件的效率
  • int sz:指定缓冲区的大小(数组长度),不指定使用默认值

特有成员方法:

  • void newLine() 写入一个行分隔符。写一个换行符号,根据系统不同,而写不同的换行符号
    • Windows系统里,每行结尾是 回车+换行 ,即 \r\n
    • linux,Unix系统里,每行结尾只有 换行 ,即 \n
    • Mac系统里,每行结尾是 回车 ,即 \r 。从 Mac OS X开始与Linux统一

使用步骤:

  • 创建BufferedWriter对象,构造方法中传递FileWriter对象

  • 使用BufferedWriter对象中的方法write,把数据写入到内存缓冲区中

  • 使用BufferedWriter对象中的方法flush,把内存缓冲区中的数据刷新到文件中

  • 释放资源(会先调用flush方法刷新数据)

5 转换流

5.1 目标

1、能够阐述编码表的意义

2、能够使用转换流读取指定编码的文本文件

3、能够使用转换流写入指定编码的文本文件

5.2 编码表

编码表就是生活中的文字和计算机中文字的对应关系。比如:

a0-> 97 -> 01100001 -> 存储到计算机中

中 -> 20013 -> 0010000000001011‬ -> 存储到计算机中

编码 :把能看懂的文字,转换成看不懂的文字,即字符-》字节

解吗: 把看不懂的文字,转换成能看懂的文字,即字节-》字符

常见的编码表:

  • ASCⅡ 字符集:英文、数字、标点符号、计算机中文字的对应关系
    • 0–>48 A–>65 a–>97
  • ISO-8859-1 字符集:拉丁码表
    • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。
    • ISO-8859-1使用单字节编码,兼容ASCII编码。不支持中文
  • GBxxx字符集:国际
    • GB就是国标的意思,是为了显示中文而设计的一套字符集。兼容ASCII表
    • GB2312:简体中文码表。 7000多个简体汉字
    • GBK:目前操作系统默认中文码表(简体,繁体),存储一个中文使用2个字节,21003个汉字
    • GB18030:最新的中文码表。包含的文字最全(简体,繁体,少数民族,日韩文字)
  • Unicode字符集 :万国码
    • UTF-8:最常用的万国表,兼容所有国家的文字
    • 编码规则:
      • 128个US-ASCII字符,只需一个字节编码。
      • 拉丁文等字符,需要二个字节编码。
      • 大部分常用字(含中文),使用三个字节编码。
      • 其他极少使用的Unicode辅助字符,使用四字节编码。

5.3 使用编码引出的问题

比如,使用FileReader 读取GBK编码的文件,会出现乱码,这是因为编码和解码不一致导致的;详细原因是:

FileReader 只能读取IDEA默认的编码文件 utf-8 编码,如果读取了GBK编码的文件,就不能正常解码,也就导致了编码和解码不一致

5.4 转换流的原理 -面试

在读取文件中的字节时,如果使用文件字符输入流FileReader 就只能查询IDEA默认的编码表utf-8,会出现乱码,而使用InputStreamReader,则会查询指定编码表,是不会出现乱码的;

同理,使用文件字符输出流FileWriter,只能将数据以IDEA默认的编码方式utf-8写出,而使用OutputStreamWriter可以查询指定的编码表,以指定的编码表写出;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DzTP4uHm-1631370637965)(Java的IO操作.assets/image-20210822124214321.png)]

5.5 InputStreamReader: 字符转换输入流

java.io.InputStreamReader:字符转换输入流 extends Reader:字符输出流

作用:

InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。解码:字节==>字符

父类的共性成员方法:

  • int read() 读取单个字符。
  • int read(char[] cbuf) 将字符读入数组。
  • void close() 关闭该流并释放与之关联的所有资源

构造方法:

  • InputStreamReader(InputStream in)创建一个使用默认字符集的 InputStreamReader
  • InputStreamReader(InputStream in, String charsetName) 创建使用指定字符集的 InputStreamReader

参数:

  • InputStream in:传递字节输入流,可以传递InputStream的任意的子类对象(读取文件中的字节)
  • String charsetName:传递编码表名称,不区分大小写的可以传递GBK(gbk),UTF-8(utf-8)…,不写默认使用IDEA设置的编码(UTF-8)

使用步骤(重点):

  • 创建InputStreamReader对象,构造方法中传递FileInputStream对象和指定的编码表名称
  • 使用InputStreamReader对象中的方法read读取文件
  • 释放资源

注意:

InputStreamReader构造方法中指定的编码表名称必须和文件的编码一致,否则会出现乱码

5.6 OutputStreamWriter: 字符转换输出流

java.io.OutputStreamWriter:字符转换输出流 extends Writer:字符输出流

作用: 搞清楚输入输出对字符和字节是如何操作的,不要搞反了

OutputStreamWriter 是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节.编码:字符==>字节

继承自父类共性的成员方法:

  • public abstract void close() :关闭此输出流并释放与此流相关联的任何系统资源
  • public abstract void flush() :刷新此输出流并强制任何缓冲的输出字符被写出
  • public void write(int c) :写出一个字符
  • public void write(char[] cbuf) :将 b.length字符从指定的字符数组写出此输出流
  • public abstract void write(char[] b, int off, int len) :从指定的字符数组写出 len字符,从偏移量 off开始输出到此输出流
  • public void write(String str) :写出一个字符串
  • public void write(String str, int off, int len) 写入字符串的某一部分

构造方法:

  • OutputStreamWriter(OutputStream out)创建使用默认字符编码的 OutputStreamWriter
  • OutputStreamWriter(OutputStream out, String charsetName) 创建使用指定字符集的 OutputStreamWriter

参数:

  • OutputStream out:传递字节输出流,可以传递OutputStream的任意子类对象(把字符转换之后的字节写入到文件中)
  • String charsetName:传递编码表名称,不区分大小写的可以传递GBK(gbk),UTF-8(utf-8)…,不写默认使用IDEA设置的编码(UTF-8)

使用步骤(重点):

  • 创建OutputStreamWriter对象,构造方法中传递FileOutputStream对象和指定的编码名称
  • 使用OutputStreamWriter对象中的方法write,把数据写入到内存缓冲区中(字符==>字节)
  • 使用OutputStreamWriter对象中的方法flush,把内存缓冲区中的数据,刷新到文件中(使用字节输出流把字节写入到文件)
  • 释放资源(会先调用flush方法刷新数据)

5.7 实例:转换文件的编码

实例 : 将GBK编码的文本文件,转换为UTF-8编码的文本文件

分析步骤:

1、创建InputStreamReader对象,构造方法中传递FileInputStream对象和GBK编码名称

2、创建OutputStreamWriter对象,构造方法中传递FIleOutputStream对象和UTF-8编码名称

3、使用InputStreamReader对象中的方法read,读取GBK编码的文件

4、使用OutputStreamWriter对象中的方法write,把读取到的数据,以UTF-8编码,写入到内存缓冲区中

5、释放资源(会先调用flush方法刷新数据到文件中)

public class Demo04Test {
    public static void main(String[] args) throws IOException {
        //1.创建InputStreamReader对象,构造方法中传递FileInputStream对象和GBK编码名称
        InputStreamReader isr = new InputStreamReader(new FileInputStream("day22\\gbk.txt"),"GBK");
        //2.创建OutputStreamWriter对象,构造方法中传递FIleOutputStream对象和UTF-8编码名称
        //OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("day22\\utf-82.txt"),"UTF-8");
        //OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("day22\\utf-83.txt"));//不写编码名称默认UTF-8
        FileWriter osw = new FileWriter("day22\\utf-84.txt");
        //3.使用InputStreamReader对象中的方法read,读取GBK编码的文件
        int len = 0;
        while ((len = isr.read())!=-1){
            //4.使用OutputStreamWriter对象中的方法write,把读取到的数据,以UTF-8编码,写入到内存缓冲区中
            osw.write(len);
        }
        //5.释放资源(会先调用flush方法刷新数据到文件中)
        osw.close();
        isr.close();
    }
}

5.8 总结

什么时候使用FileReader和FileWriter(读取字符的便捷类): 读写的文件都是IDEA默认编码utf-8的文件;

什么时候使用InputStreamReader和OutputStreamWriter: 读写的文件不是IDEA默认编码utf-8的文件;

6 序列化流

6.1 目标

1、能够使用序列化流写出对象到文件

2、能够使用反序列化流读取文件到程序中

6.2 序列化流和反序列化流的概述

序列化流:

把对象以流的方式写入到文件中保存,叫写对象,也叫对象的序列化,写入的不仅仅包含字符,还包含一些其他内容,所以得使用字节流,一般使用ObjectOutputStream 对象的输出流,也叫对象的序列化流;

反序列化流:

把文件中保存的对象,以流的方式读取出来使用,叫读对象,也叫对象的反序列化,读取的数据不仅仅是字符,还有一些其他的内容,所以得使用字节流,一般使用ObjectInputStream 对象的输入流,也叫对象的反序列化流;

我们将对象序列化到文件中,这个时候文件保存的都是字节,如果直接打开,可能会出现乱码,这事因为没有以对应的编码方式打开,也就是解码的方式不对;

6.3 ObjectOutputStream 对象的序列化流

java.io.ObjectOutputStream:对象的序列化流 extends OutputStream:字节输出流

作用: 把对象以流的方式写入到文件中保存

构造方法:

  • ObjectOutputStream(OutputStream out) 创建写入指定 OutputStream 的 ObjectOutputStream
  • 参数: OutputStream out:传递字节输出流,可以传递OutputStream的任意子类对象

特有的成员方法:

  • void writeObject(Object obj) 将指定的对象写入 ObjectOutputStream

使用步骤:

  • 1、创建ObjectOutputStream对象,构造方法中传递FileOutputStream对象
  • 2、使用ObjectOutputStream对象中的方法writeObejct,把对象写入到文件中
  • 3、释放资源

注意:NotSerializableException

在进行序列化|反序列化的时候,会抛出NotSerializableException:没有序列化异常

类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。

Serializable接口是一个标记型接口,类实现了Serializable接口,接口就会给类添加上一个标记

当我们进行序列化和反序列化的时候,会检查类上是否有标记,有则序列化反序列化成功,没有会抛出异常

比如:

去市场买肉=>肉上会有一个检查合格的章=>放心购买=>买回来肉怎么吃=>随意:炖,炒,刷…

类=>添加上了标记=>放心进行序列化和反序列化了=>类做不做其他的用途没有影响

6.4 ObjectInputStream 对象的反序列化流

java.io.ObjectInputStream:对象的反序列化流 extends InputStream:字节输入流

作用: 把文件中保存的对象以流的方式读取出来使用

构造方法:

  • ObjectInputStream(InputStream in) 创建从指定 InputStream 读取的 ObjectInputStream
  • 参数: InputStream in:传递字节输入流,我们可以传递InputStream的任意子类对象

特有的成员方法:

  • Object readObject() 从 ObjectInputStream 读取对象,readObject方法声明抛出了两个异常对象:IOException, ClassNotFoundException

使用步骤:

  • 1、创建ObjectInputStream对象,构造方法中传递FileInputStream对象
  • 2、使用ObjectInputStream对象中的方法readObject,读取文件中保存的对象
  • 3、释放资源

注意:

  • 反序列化有两个前提:
    • 1.类实现Serializable接口
    • 2.类有对应的class文件(Person.class)
public class Demo02ObjectInputStream {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //1.创建ObjectInputStream对象,构造方法中传递FileInputStream对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day22\\person.txt"));
        //2.使用ObjectInputStream对象中的方法readObject,读取文件中保存的对象
        Object obj = ois.readObject();
        System.out.println(obj);

        Person p = (Person)obj;
        p.setAge(20);
        System.out.println(p);
        //3.释放资源
        ois.close();
    }
}

6.5 瞬态关键字 transient

transient:瞬态关键字

被transient关键字修饰的变量不能被序列化

static:静态关键字

被static修饰的成员属于类,不属于某一个对象,被所有的对象所共享使用

被static修饰的成员,也不能序列化,序列化的是对象

6.6 序列号冲突异常-面试

编译器javac.exe会把自定义类文件,比如Person.java ,编译生成Person.class文件,Person类实现了 Serializable 接口,就会根据类的定义给Person.class文件添加一个序列号,但是每次修改了类的定义,那么就会给Person.class文件,重新编译生成一个新的序列号,反序列化的时候,就会使用Person.class文件中的序列号和Person.txt文件中的序列号进行比较,如果一样,反序列化成功,如果不一样,则抛出序列化冲突异常:InvalidClassException

解决方案:无论是否对类的定义进行了修改,都不重新生成新的序列号,我们可以手动的给类添加一个序列号来实现,这样无论类是否修改, 序列号都是固定写死的常量值, 值是不会改变的, 也就不会抛出异常了

实现步骤:

  • 1、该类实现了Serializable接口,这是前提;

  • 2、在类的成员变量位置,新增一个 serialVersionUID 字段,并使用final static修饰,数据类型使用long,以此来显式声明其字节的serialVersionUID;

6.7 实例:序列化集合

如果要往文件中存储多个对象, 不建议使用多次writeObject方法写多个对象(读取多次),建议创建一个集合, 把对象保存到集合中, 对集合对象进行序列化和反序列化(读写一次就行了)

import java.io.*;
import java.util.ArrayList;

public class Demo03Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        show02();
    }

    /*
        序列化多个对象,反序列化多个对象,使用集合
     */
    private static void show02() throws IOException, ClassNotFoundException {
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person("张三",18));
        list.add(new Person("李四",19));
        list.add(new Person("王五",20));
        list.add(new Person("赵六",21));
        list.add(new Person("田七",22));
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day22\\list02.txt"));
        //序列化集合对象
        oos.writeObject(list);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day22\\list02.txt"));
        //读取文件中保存的集合对象,就一个,读取一次
        Object obj = ois.readObject();
        //遍历集合==>向下转型==>把Object类型转换为ArrayList<Person>
        ArrayList<Person> list02 = (ArrayList<Person>)obj;
        for (Person p : list02) {
            System.out.println(p);
        }
    }
}

7 打印流

java.io.PrintStream:字节打印流 extends OutputStream:字节输出流

特点:

  • 1.PrintStream 为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。PrintStream流中有两个特有的方法:print,println
  • 2.与其他输出流不同,PrintStream 永远不会抛出 IOException,创建对象的时候,传递路径不存在,可能会抛出文件找不到异常
  • 3.PrintStream叫打印流,只负责输出(打印),不能读取

构造方法:

  • PrintStream(File file) 打印流的目的地是一个文件
  • PrintStream(OutputStream out) 打印流的目的地是一个字节输出流
  • PrintStream(String fileName) 打印流的目的地是一个文件路径

成员方法:

  • 1、继承自父类OutputStream的共性的成员方法
    • write:使用write写数据,查看的时候,会查询编码表
    • public void close() :关闭此输出流并释放与此流相关联的任何系统资源
    • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出
    • public void write(byte[] b) :将 b.length字节从指定的字节数组写入此输出流
    • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流
    • public abstract void write(int b) :将指定的字节输出流
  • 自己特有的成员方法: print, println写数据,原样输出
    • void print(Object x) 可以写任意类型数据,不换行
    • void println(Object x) 可以写任意类型的数据,换行

使用步骤(重点)::

  • 1.创建PrintStream对象,构造方法中绑定要输出的目的地
  • 2.使用PrintStream对象中的方法write,print,println,把数据写入到文件中
  • 3.释放资源
public class Demo01PrintStream {
    public static void main(String[] args) throws FileNotFoundException {
        //1.创建PrintStream对象,构造方法中绑定要输出的目的地
        PrintStream ps = new PrintStream("day22\\print1.txt");
        //2.使用PrintStream对象中的方法write,print,println,把数据写入到文件中
        //继承自父类OutputStream的共性的成员方法write:使用write写数据,查看的时候,会查询编码表
        ps.write(97);//a

        //自己特有的成员方法:print,println写数据,原样输出
        ps.println(97);//97
        ps.println('@');//@
        ps.println(true);//true
        ps.println(1.1);//1.1
        ps.println("aaa");//aaa

        //3.释放资源
        ps.close();

        //打印字符数组不是地址值,是数组中的元素 void println(char[] x) 打印字符数组(字符串底层就是字符数组)
        char[] chars = {'a','b','c'};
        System.out.println(chars);//abc
        System.out.println("abc");//abc
    }
}

修改输出语句的目的地为打印流的目的地

import java.io.FileNotFoundException;
import java.io.PrintStream;

/*
    修改输出语句的目的地为打印流的目的地
    使用System类中的方法setOut,修改输出语句的目的地(控制台)为打印流的目的地
	static void setOut(PrintStream out) 重新分配“标准”输出流。
	作用:不让输出语句在控制台输出了,在打印流的目的地文件中输出
 */
public class Demo02PrintStream {
    public static void main(String[] args) throws FileNotFoundException {
        System.out.println("输出语句的目的地默认是在控制台输出!");

        PrintStream ps = new PrintStream("day22\\print2.txt");
        System.setOut(ps);//修改输出语句的目的地(控制台)为打印流的目的地
        System.out.println(1);
        System.out.println(2);
        System.out.println(3);
        System.out.println("输出语句的目的地不是控制台了,是print2.txt文件中");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值