【JavaSE】IO之字符流与编码机制
上一篇文章中,我们介绍了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,而是先读字节,再进行编码
字符流的底层工作流程可以拆成三步:
- 从文件中读取原始字节数据
- 按照指定的字符编码解析字节
- 将解析后的字符以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,再关闭流
至此可以看到,字符流在字节流之上,额外承担了两件事:编码转换和缓冲优化
更多推荐



所有评论(0)