上一篇文章中,我们介绍了Java IO中的字节流,并通过InputStream / OutputStream 完成了文件的读写操作

字节流以byte为最小单位,能够精确地操作文件中地原始数据,因此非常适合处理图片、视频等二进制文件

而在实际开发中,我们更多面对的是文本文件,此时开发者真正关心的往往不是“字节”,而是“字符”

为了解决文本读写以及字符编码相关的问题,Java在字节流之上,有提供了一套字符流的抽象

一句话来说,就是在字节流之上,由编码参与的IO操作

一、Java字符流体系

和字节流类似,Java也有两类字符流,对应的顶级抽象类分别是Reader(字符输入流)和Writer(字符数据流)

Java中所有的字符流对象,都是直接或间接继承自Reader和Writer这两个抽象类

二、Reader —— 字符输入流

2.1 打开文件

Reader reader = new FileReader("text.txt");

这里创建一个FileReader对象,一旦创建成功,就相当于打开了一个文本文件

2.2 读取文件内容

Reader同样通过read方法来读取书,常用的重载形式包括

  • int read()
  • int read(char[] cbuf)
  • int read(char[] cbuf, int off, int len)

此时返回值为该字符的Unicode编码值,当返回-1时,表示文件已经读取结束

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class ReaderReadDemo {

    public static void main(String[] args) {

        try (Reader reader = new FileReader("test.txt")) {

            int ch;
            while ((ch = reader.read()) != -1) {
                System.out.print((char) ch);
            }

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

2.3 使用字符数组读取

单个字符读取同样会遇到效率较低的问题,我们可以使用字符数组来一次性读取一组字符

char[] buffer = new char[1024];
int len;

while ((len = reader.read(buffer)) != -1) {
    System.out.print(new String(buffer, 0, len));
}

这一写法与字节流中的read(byte[]) 在使用方式上完全一致,只是读取单位由byte变成了char

2.4 字符流为什么读得更少?

在使用IO时,大家可能会有这样一个疑问:同样是读取文件,为什么字符流看起来“读得更少”?

我们来复现一下,创建一个text.txt文件,里面写着“你好”
在这里插入图片描述
先使用字节流读取

package file;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Demo{
    public static void main(String[] args) throws IOException {
        try(InputStream inputStream = new FileInputStream("./text.txt")){
            int num = 0;
            while(inputStream.read() != -1){
                System.out.println(num++);
            }
        }
    }
}

在这里插入图片描述
可以看到读取了6次,每次读取一个字节,也就是六个字节

接下来我们使用字符流读取

package file;

import java.io.*;

public class Demo12 {
    public static void main(String[] args) throws IOException {
        try(Reader reader = new FileReader("./text.txt")){
            int num = 0;
            while(reader.read() != -1){
                System.out.println(num++);
            }
        }
    }
}

可以看到只读取了两次
在这里插入图片描述
那么问题来了,文件里明明是 6 个字节,为什么字符流“只读出了 2 次”?

核心在于,字符流并不是直接从文件中读取char,而是先读字节,再进行编码

字符流的底层工作流程可以拆成三步:

  1. 从文件中读取原始字节数据
  2. 按照指定的字符编码解析字节
  3. 将解析后的字符以Unicode的形式返回给数据

也就是说,字符流并没有少读,而是它把多个字节合并成了一个“字符级” 的结果

那我们上面的例子来说,文件中存储的是UTF-8编码,字符流读取到这三个字节后,根据UTF-8编码规则查UTF-8码表,解析出对应的Unicode码点

而在Unicode中,大多数常用字符都可以用一个char表示,因此就出现了文件中六个字节,读取四个字节的情况

层级 占用
文件中(UTF-8) 3 字节
Java 内部(char) 2 字节

这并不是“数据丢失”,而是编码体系不同导致的表现差异

三、Writer —— 字符输出流

在前面我们介绍了 Reader(字符输入流),可以看到字符流在读取文本时,会自动完成「字节 → 字符」的解码过程。

与 Reader 对应的,Java 提供了 Writer(字符输出流),用于完成「字符 → 字节」的编码与写入。

如果用一句话来概括两者的关系:Reader 是“读字节 + 解码”,Writer 是“编码 + 写字节”

3.1 打开文件

Writer writer = new FileWriter("text.txt");

这里创建一个FileWriter对象,一旦创建成功,就相当于打开一个文本文件并准备向其中写入字符数据

与FileOutputStream类似,文件不存在时会自动创建,文件已存在则默认清空原有内容

3.2 写入文件内容

Writer同样通过writer方法完成写操作

  • void write(int c):写入一个字符
  • void write(char[] cbuf):写入字符数组
  • void write(char[] cbuf, int off, int len):写入指定范围字符
  • void write(String str):写入字符串(最常用)
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class WriterWriteDemo {
    public static void main(String[] args) {

        try (Writer writer = new FileWriter("text.txt")) {

            writer.write("你好");
            writer.write("\n");
            writer.write("Java 字符流");

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

此时写入的就是字符级的数据

3.3 使用字符数组写入

与 Reader 中使用 char[] 提高读取效率类似,Writer 也可以一次写入一组字符

char[] data = {'你', '好', '\n'};
writer.write(data);

3.4 追加写模式

默认情况下,FileWriter采用的是覆盖写模式

如果希望在原有内容基础上继续写入,可以执行追加写模式

Writer writer = new FileWriter("text.txt", true);

与Reader类似,Writer看起来也会写得更少

因为Writer写入的是字符,真正落盘的是编码后的字节

3.5 flush与close

Writer通常内部带有缓冲区,为了减少系统调用次数,提高IO性能

这里的系统调用,指的是将字符写入文件会触发一次操作系统级别的IO,会进行用户态到内核态的切换
而系统调用是非常昂贵的操作,因此JVM不直接将字符写入文件,而是先存进内存中的缓冲区,当缓冲区满了或者程序调用flush()或者调用close()时才真正写入文件

  • write():写入缓冲区
  • flush():强制将缓冲区内容写入文件
  • close():先 flush,再关闭流

至此可以看到,字符流在字节流之上,额外承担了两件事:编码转换缓冲优化

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐