Java的IO流

IO流

  • 字节流
  • 字符流
  • 转换流
  • 缓冲流
  • 数据流
  • 读写对象(序列化)
  • NIO

在大多数程序中,都需要对输入输出进行处理。例如我们中需要获取用户从键盘上的输入,需要在控制台输出结果等等。除此之外还有从文件中读取数据,向文件中写入数据等等。在 Java 中,我们把这些不同类型的输入输出源抽象地称为,也就是Stream;在里面输入输出的数据则称为数据流Data Stream),它们通常具有统一的接口。

于是我们得到了数据流的定义:

一个 Java I/O 对象叫做数据流。读取数据到内存的对象叫做输入流,内存写出数据的对象叫做输出流。

针对其面向的不同角度,我们大致可以将流分为下面几种类型:

  • 按照数据流的方向不同分为输入流输出流。这种分类不是绝对的,例如在向一个文件写入数据时,它就是输出流;而在读取数据时,它就是输入流。
  • 按照处理数据的单位不同分为字节流字符流
  • 按照功能的不同分为节点流处理流

需要特别说明,节点流是从特定的数据节点(文件、数据库、内存等)读写数据;处理流是连接在已有的流上,通过对数据的处理为程序提供更多功能。

在 Java 环境中,java.io包提供了大多数的类和接口来实现输入输出管理。一些标准的输入输出则来自java.lang包中的类,但它们都是继承自java.io中的类。我们可以将输入流理解为数据的提供者,而把输出流理解为数据的接收者。在最初的时候,这些派生自抽象类InputStreamOutputStream的输入输出类是面向 8 位的字节流的。但为了支持国际化,又引入了派生自抽象类ReaderWriter的类层次,用于读写一些双字节的Unicode字符。

因此,在学习 java 的输入输出上,我们希望你以字节流和字符流作为区分来学习。

如果需要概括一下,则可以得到下面的定义:

  • 字节流:表示以字节为单位从 stream 中读取或往 stream 中写入信息。通常用来读取二进制数据。
  • 字符流:以 Unicode 字符为单位从 stream 中读取或往stream 中写入信息。

按照这样的定义,Java 中流的层级结构可以通过下图来表示

图中蓝色的部分均为抽象类,而绿色的部分则为派生类,是可以直接使用的。

而下图简要说明了字节流和字符流的区别,你也可以进一步了解字节流与字符流的区别

我们知道 Java 是一门面向对象的语言,所以为了能够永久地保存对象的状态,java.io包还以字节流为基础,通过实现ObjectInputObjectOutput接口提供了对象流。在此仅作引入,你可以通过查阅 API 手册来详细了解它们。

字节流

字节流主要操作 byte 类型数据,以 byte 数组为准,java 中每一种字节流的基本功能依赖于基本类 InputStreamOutputstream,他们是抽象类,不能直接使用。字节流能处理所有类型的数据(如图片、avi等)。

InputStream

InputStream 是所有表示字节输入流的父类,继承它的子类要重新定义其中所定义的抽象方法。InputStream 是从装置来源地读取数据的抽象表示,例如 System 中的标准输入流 in 对象就是一个 InputStream 类型的实例。

InputStream 类方法:

方法说明
read()throws IOException从输入流中读取数据的下一个字节(抽象方法)
skip(long n) throws IOException跳过和丢弃此输入流中数据的 n 个字节
available()throws IOException返回流中可用字节数
mark(int readlimit)throws IOException在此输入流中标记当前的位置
reset()throws IOException将此流重新定位到最后一次对此输入流调用 mark 方法时的位置
markSupport()throws IOException测试此输入流是否支持 mark 和 reset 方法
close()throws IOException关闭流

在 InputStream 类中,方法 read() 提供了三种从流中读数据的方法:

  1. int read():从输入流中读一个字节,形成一个 0~255 之间的整数返回(是一个抽象方法)
  2. int read(byte b[]):从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
  3. int read(byte b[],int off,int len):从输入流中读取长度为 len 的数据,写入数组 b 中从索引 off 开始的位置,并返回读取得字节数。

对于这三个方法,若返回-1,表明流结束,否则,返回实际读取的字符数。

OutputStream

OutputStream 是所有表示位输出流的类之父类。子类要重新定义其中所定义的抽象方法,OutputStream 是用于将数据写入目的地的抽象表示。例如 System 中的标准输出流对象 out 其类型是java.io.PrintStream,这个类是 OutputStream 的子类。

OutputStream 类方法:

方法说明
write(int b)throws IOException将指定的字节写入此输出流(抽象方法)
write(byte b[])throws IOException将字节数组中的数据输出到流中
write(byte b[], int off, int len)throws IOException将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流
flush()throws IOException刷新此输出流并强制写出所有缓冲的输出字节
close()throws IOException关闭流
练习
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class Test {

    /**
     * 把输入流中的所有内容赋值到输出流中
     * @param in
     * @param out
     * @throws IOException
     */
    public void copy(InputStream in, OutputStream out) throws IOException {
        byte[] buf = new  byte[4096];
        int len = in.read(buf);
        //read 是一个字节一个字节地读,字节流的结尾标志是-1
        while (len != -1){
            out.write(buf, 0, len);
            len = in.read(buf);
        }
    }
    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        Test t = new Test();
        System.out.println("输入字符:");
        t.copy(System.in, System.out);
    }

}

练习题:文件分割

/home/project/目录下新建FileCut.java,你需要实现以下需求:

  • 从控制台读取一个数值 n。
  • /home/project目录下新建一个文本文件 cut.txt,填入任意内容,尽量多输入一些字符。
  • 将 cut.txt 文件平均分割,每份文件大小为 n 字节。
  • 分割后的文件分别命名为 cut1.txt、cut2.txt … cutn.txt 保存在/home/project目录下。

提示:

  • 获取文件所占字节大小,根据字节平均分割文件
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;

public class FileCut {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        File file = new File("/home/project/file.txt");
        //需要分隔的文件份数
        int num;
        //如果不能整除,那么需要多加一个文件 用于保存剩余的数据
        if (file.length() % n == 0) {
            num = (int) (file.length() / n);
        } else {
            num = (int) (file.length() / n) + 1;
        }
        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            byte[] bytes = new byte[(int) file.length()];
            //读取文件到bytes
            fileInputStream.read(bytes);
            fileInputStream.close();
            for (int i = 1; i <= num; i++) {
                //文件名
                String fileName = "/home/project/cut" + i + ".txt";
                FileOutputStream fileOutputStream = new FileOutputStream(fileName);
                //最后一份文件需要特殊处理 因为他的大小不是n
                if (i == num) {
//                    (file.length()-n*(i-1)) 文件的总字节数 再减去前面已经读取的字节数 就是剩余的字节数
                    fileOutputStream.write(bytes, n * (i - 1), (int) (file.length() - n * (i - 1)));
                } else {

                    fileOutputStream.write(bytes, n * (i - 1), n);
                }
                fileOutputStream.flush();
                fileOutputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符流

字符流以字符为单位,根据码表映射字符,一次可能读多个字节,只能处理字符类型的数据。

java.io 包中专门用于字符流处理的类,是以 Reader 和 Writer 为基础派生的一系列类。

同类 InputStream 和 OutputStream 一样,Reader 和 Writer 也是抽象类,只提供了一系列用于字符流处理的接口。它们的方法与类 InputStream 和 OutputStream 类似,只不过其中的参数换成字符或字符数组。

Reader 是所有的输入字符流的父类,它是一个抽象类。

我们先来看一看基类 Reader 的方法,其用法与作用都与 InputStream 和 OutputStream 类似,就不做过多的说明了。

方法返回值
close()void
mark (int readAheadLimit)void
markSupported()boolean
read()int
read(char[] cbuf, int off,int len)int
ready()boolean
reset()void
skip(long n)long

Writer 是所有的输出字符流的父类,它是一个抽象类。 Writer 的方法:

方法返回值
close()void
flush()void
write(char[] cbuf)void
write(char[] cbuf, int off,int len)void
write(int c)void
write(String str)void
write(String str, int off, int len)void

在这里我们就列举一下有哪些类。

  1. 对字符数组进行处理: CharArrayReader、CharArrayWrite
  2. 对文本文件进行处理:FileReader、FileWriter
  3. 对字符串进行处理:StringReader、StringWriter
  4. 过滤字符流:FilterReader、FileterWriter
  5. 管道字符流:PipedReader、PipedWriter
  6. 行处理字符流:LineNumberReader
  7. 打印字符流:PrintWriter

类有千万,方法更是不计其数,所以没有必要去掌握所有的方法和类,只需要知道常见常用的就行了,而大多数的类和方法,希望大家有一个印象,当我们在实际开发的时间,能够想到,并且借助其他工具去查询我们需要的方法的应用方式就可以了。

转换流

InputStreamReader 和 OutputStreamWriter 是 java.io 包中用于处理字符流的最基本的类,用来在字节流和字符流之间作为中介:从字节输入流读入字节,并按编码规范转换为字符;往字节输出流写字符时先将字符按编码规范转换为字节。使用这两者进行字符处理时,在构造方法中应指定一定的平台规范,以便把以字节方式表示的流转换为特定平台上的字符表示。

InputStreamReader(InputStream in); //缺省规范说明

//指定规范 enc
InputStreamReader(InputStream in, String enc);

OutputStreamWriter(OutputStream out); //缺省规范说明

//指定规范 enc
OutputStreamWriter(OutputStream out, String enc);

如果读取的字符流不是来自本地时(比如网上某处与本地编码方式不同的机器),那么在构造字符输入流时就不能简单地使用缺省编码规范,而应该指定一种统一的编码规范“ISO 8859_1”,这是一种映射到 ASCCII 码的编码方式,能够在不同平台之间正确转换字符。

InputStreamReader ir = new InputStreamReader(is,"8859_1");

缓冲流

1:定义:在内存与硬盘之间创建一个大小合适的缓冲区,当内存和硬盘进行数据访问时,能提高访问硬盘的次数,提高效率。
2:分类:缓冲分为字节缓冲流(BufferedInputStreamBufferedOutputStream)和字符缓冲流(BufferedReaderBufferedWrite)。

在初始化时,除了要指定所连接的 I/O 流之外,还可以指定缓冲区的大小。缺省时是用32字节大小的缓冲区;最优的缓冲区大小常依赖于主机操作系统、可使用的内存空间以及机器的配置等;一般缓冲区的大小为内存页或磁盘块等的整数倍。

BufferedInputStream 的数据成员 buf 是一个位数组,默认为 2048 字节。当读取数据来源时例如文件,BufferedInputStream 会尽量将 buf 填满。当使用 read ()方法时,实际上是先读取 buf 中的数据,而不是直接对数据来源作读取。当 buf 中的数据不足时,BufferedInputStream 才会再实现给定的 InputStream 对象的 read() 方法,从指定的装置中提取数据。

BufferedOutputStream 的数据成员 buf 是一个位数组,默认为 512 字节。当使用 write() 方法写入数据时,实际上会先将数据写至 buf 中,当 buf 已满时才会实现给定的 OutputStream 对象的 write() 方法,将 buf 数据写至目的地,而不是每次都对目的地作写入的动作。

//[ ]里的内容代表选填
BufferedInputStream(InputStream in [, int size])
BufferedOutputStream(OutputStream out [, int size])

举个例子,将缓冲流与文件流相接:

FileInputStream in = new FileInputStream("file.txt");
FileOutputStream out = new FileOutputStream("file2.txt");
//设置输入缓冲区大小为256字节
BufferedInputStream bin = new BufferedInputStream(in,256)
BufferedOutputStream bout = new BufferedOutputStream(out,256)
int len;
byte bArray[] = new byte[256];
len = bin.read(bArray); //len 中得到的是实际读取的长度,bArray 中得到的是数据

对于 BufferedOutputStream,只有缓冲区满时,才会将数据真正送到输出流,但可以使用 flush() 方法人为地将尚未填满的缓冲区中的数据送出。

例如方法 copy():

public void copy(InputStream in, OutputStream out) throws IOException {
    out = new BufferedOutputStream(out, 4096);
    byte[] buf = new byte[4096];
    int len = in.read(buf);
    while (len != -1) {
    out.write(buf, 0, len);
    len = in.read(buf);
    }
    //最后一次读取得数据可能不到4096字节
    out.flush();
}
BufferedReader和BufferedWrite

同样的,为了提高字符流处理的效率,java.io 中也提供了缓冲流 BufferedReader 和 BufferedWrite。其构造方法与 BufferedInputStreamBufferedOutPutStream 相类似。另外,除了 read() 和 write() 方法外,它还提供了整行字符处理方法:

  1. public String readLine():BufferedReader 的方法,从输入流中读取一行字符,行结束标志\n\r或者两者一起(这是根据系统而定的)
  2. public void newLine():BufferedWriter 的方法,向输出流中写入一个行结束标志,它不是简单地换行符\n\r,而是系统定义的行隔离标志(line separator)。

看一看例子吧:

// FileToUnicode.java
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class FileToUnicode {
    public static void main(String args[]) {
        try {
            FileInputStream fis = new FileInputStream("file1.txt");
            InputStreamReader dis = new InputStreamReader(fis);
            BufferedReader reader = new BufferedReader(dis);
            String s;
            //每次读取一行,当改行为空时结束
            while((s = reader.readLine()) != null){
                System.out.println("read:" + s);
            }
            dis.close();
        }
        catch(IOException e) {
            System.out.println(e);
        }
    }
}

如 file1.txt 的内容如下:

abc
efg
hij

数据流

1:定义:数据流是对8大基本数据类型的数据进行内存与硬盘之间互相访问的流,属于包装流,包装后使原来的流传输数据更加方便
2:分类:数据流只有字节流,没有字符流。

接口 DataInputDataOutput,设计了一种较为高级的数据输入输出方式:除了可处理字节和字节数组外,还可以处理 int、float、boolean 等基本数据类型,这些数据在文件中的表示方式和它们在内存中的一样,无须转换,如 read(), readInt(), readByte()...; write(), writeChar(), writeBoolean()...此外,还可以用readLine()方法读取一行信息。

常用方法
方法返回值说明
readBoolean()boolean
readByte()byte
readShort()short
readChar()char
readInt()int
readLong()long
readDouble()double
readFloat()float
readUnsignedByte()int
readUnsignedShort()int
readFully(byte[] b)void从输入流中读取一些字节,并将它们存储在缓冲区数组 b 中
reaFully(byte[] b, int off,int len)void从输入流中读取 len 个字节
skipBytes(int n)int与 InputStream.skip 等价
readUTF()String按照 UTF-8 形式从输入中读取字符串
readLine()String按回车(\r)换行(\n)为分割符读取一行字符串,不完全支持 UNICODE
writeBoolean(boolean v)void
writeByte(int v)void
writeShort(int v)void
writeChar(int v)void
writeInt(int v)void
writeLong(long v)void
writeFloat(float v)void
writeDouble(double v)void
write(byte[] b)void与 OutputStream.write 同义
write(byte[] b, int off, int len)void与 OutputStream.write 同义
write(int b)void与 OutputStream.write 同义
writeBytes(String s)void只输出每个字符的低 8 位;不完全支持 UNICODE
writeChars(String s)void每个字符在输出中都占两个字节

数据流类 DataInputStreamDataOutputStream 的处理对象除了是字节或字节数组外,还可以实现对文件的不同数据类型的读写:

  1. 分别实现了 DataInputDataOutput 接口
  2. 在提供字节流的读写手段同时,以统一的形式向输入流中写入 boolean,int,long,double 等基本数据类型,并可以再次把基本数据类型的值读取回来。
  3. 提供了字符串读写的手段

数据流可以连接一个已经建立好的数据对象,例如网络连接、文件等。数据流可以通过如下方式建立:

FileInputStream fis = new FileInputStream("file1.txt");
FileOutputStream fos = new FileOutputStream("file2.txt");
DataInputStream dis = new DataInputStream(fis);
DataOutputStream dos = new DataOutputStream(fos);
编程实战

/home/project/目录下新建源代码文件DataStream.java

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataStream {

    public static void main(String[] args) throws IOException{
        //向文件 a.txt 写入
        FileOutputStream fos = new FileOutputStream("a.txt");
        DataOutputStream dos = new DataOutputStream(fos);
        try {
            dos.writeBoolean(true);
            dos.writeByte((byte)123);
            dos.writeChar('J');
            dos.writeDouble(3.1415926);
            dos.writeFloat(2.122f);
            dos.writeInt(123);
        }
        finally {
            dos.close();
        }
        //从文件 a.txt 读出
        FileInputStream fis = new FileInputStream("a.txt");
        DataInputStream dis = new DataInputStream(fis);
        try {
            System.out.println("\t" + dis.readBoolean());
            System.out.println("\t" + dis.readByte());
            System.out.println("\t" + dis.readChar());
            System.out.println("\t" + dis.readDouble());
            System.out.println("\t" + dis.readFloat());
            System.out.println("\t" + dis.readInt());
        }
        finally {
            dis.close();
        }
    }

}

编译运行:

$ javac DataStream.java
$ java DataStream
        true
        123
        J
        3.1415926
        2.122
        123

读写对象

1: 定义:对象流传输的是一个对象,在我们需要保存一个对象的很多属性的时候,用对象流可以简化代码,更加方便2:分类:输入对象流和输出对象流

2:对象流也属于包装流,包装后使原来的流具有对对象进行读写的功能

我们知道实例化的对象存在于内存中,如果我们想传输实例化的对象怎么办呢?可以通过 ObjectOutputStreamObjectInputStream 将对象输入输出。 将对象的状态信息转换为可以存储或者传输的形式的过程又叫序列化

/home/project目录下新建一个源代码文件ReadWriteObject.java

import java.io.*;

public class ReadWriteObject {
    public static void main(String[] args) {
        File file = new File("/home/project/user.file");
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file))) {
            //将匿名对象 写入到file中,注意:被写入的对象必须实现了Serializable接口
            objectOutputStream.writeObject(new User("shiyanlou", "password"));
            objectOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        //读取文件 打开输入流
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file))) {
//            将信息还原为user实例
            User user = (User) objectInputStream.readObject();
            //打印user信息  和上面创建的匿名对象的信息一致
            System.out.println(user.toString());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

//静态内部类 必须实现Serializable
    static class User implements Serializable {
        private String username;
        private String password;

        public User(String username, String password) {
            this.username = username;
            this.password = password;
        }

        @Override
        public String toString() {
            return "User{" +
                    "username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    }
}

编译运行:

$ javac ReadWriteObject.java
$ java ReadWriteObject
User{username='shiyanlou', password='password'}

NIO

Java NIO(New IO)发布于 JDK1.4,用于代替 Java 标准 IO 。Java NIO是面向缓存的、非阻塞的IO,而标准IO是面向流的,阻塞的IO。

首先理解 NIO 的重要概念-Buffer(缓冲区)

  • NIO 读取或者写入数据都要通过 Buffer
  • 通过 allocate()方法分配 Buffer,Buffer 不可实例化,Buffer 是抽象类,需要使用具体的子类,比如 ByteBuffer
  • Buffer 的参数
    • capacity :缓冲区的容量
    • position :当前指针位置,没读取一次缓冲区数据或者写入缓冲区一个数据那么指针将会后移一位
    • limit :限制指针的移动,指针不能读取 limit 之后的位置​ - mark :如果设置该值,那么指针将移动到 0~position 的位置
    • 最后可以这几个参数的关系如下:mark <= position <= limit <= capacity

编程实例

学习编程的最好方式当然是通过代码来学习,接下来将使用 NIO 来完成的文件的读取和写入操作,同学们可以对比标准 IO 的基本操作理解其不同。

/home/project/目录下新建源代码文件NioDemo.java

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Scanner;

public class NioDemo {
    public static void main(String[] args) {
        try {
            File file = new File("/home/project/nio.txt");
            if (!file.exists()) {
                file.createNewFile();
            }
            //创建channel  nio通过channel来连接文件 相当于桥梁
            FileChannel writeChannel = new RandomAccessFile(file, "rw").getChannel();
            //创建一个ByteBuffer 容量为100
            ByteBuffer byteBuffer = ByteBuffer.allocate(100);
            System.out.println("请输入字符串");
            Scanner in = new Scanner(System.in);
            String s = in.nextLine();
            //将字符串写入到缓冲区
            byteBuffer.put(s.getBytes());
            System.out.println("写入数据后指针变化-position:" + byteBuffer.position() + " limit:" + byteBuffer.limit() + " capacity :" + byteBuffer.capacity());
            //为输出数据做准备 将limit移动到position位置,position置0
            byteBuffer.flip();
            System.out.println("flip后指针变化-position:" + byteBuffer.position() + " limit:" + byteBuffer.limit() + " capacity :" + byteBuffer.capacity());
            //将缓冲区写入channel
            writeChannel.write(byteBuffer);
            //清除缓冲区 为下次写入或者读取数据做准备 恢复到初始状态 position=0 limit=capacity=100  因为我们这里分配的容量大小为100
            byteBuffer.clear();
            System.out.println("clear后指针变化-position:" + byteBuffer.position() + " limit:" + byteBuffer.limit() + " capacity :" + byteBuffer.capacity());
            //关闭channel
            writeChannel.close();
            FileChannel readChannel = new RandomAccessFile(file, "r").getChannel();
            //从channel中将数据读取到缓冲区
            while (readChannel.read(byteBuffer) != -1) {
                //为读取数据做准备
                byteBuffer.flip();
                //输出数据 设置解码器
                Charset charset = Charset.forName("UTF-8");
                CharsetDecoder decoder = charset.newDecoder();
                System.out.println("读取结果:" + decoder.decode(byteBuffer));
                //清除缓冲区
                byteBuffer.clear();
            }
            readChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
$ javac NioDemo.java
$ java NioDemo
请输入字符串
shiyanlou
写入数据后指针变化-position:9 limit:100 capacity :100
flip后指针变化-position:0 limit:9 capacity :100
clear后指针变化-position:0 limit:100 capacity :100
shiyanlou
   Charset charset = Charset.forName("UTF-8");
            CharsetDecoder decoder = charset.newDecoder();
            System.out.println("读取结果:" + decoder.decode(byteBuffer));
            //清除缓冲区
            byteBuffer.clear();
        }
        readChannel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

}


$ javac NioDemo.java
$ java NioDemo
请输入字符串
shiyanlou
写入数据后指针变化-position:9 limit:100 capacity :100
flip后指针变化-position:0 limit:9 capacity :100
clear后指针变化-position:0 limit:100 capacity :100
shiyanlou


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值