字符流
本教程共 100 篇 · 第 87 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
本节目标:用 Reader 和 Writer 正确读写文本文件,彻底搞明白中文乱码是怎么来的、又该怎么根治。
为什么需要字符流
上一章的字节流已经能读写任何文件了。既然如此,为什么还要单独搞一套字符流?
因为文本有编码问题。
Java 内部用 Unicode 存字符,一个 char 占 16 位。但文件里存的是字节。UTF-8 编码下,一个英文字母占 1 字节,一个汉字占 3 字节。
用字节流读中文文本,你拿到的是一堆零散字节,得自己拼装、自己解码。字符流把这活儿全包了。
字符流 = 字节流 + 自动编码转换。
Reader 与 Writer
字符流的两个抽象基类是 Reader 和 Writer,方法签名跟字节流长得很像:
public int read() throws IOException; // Reader
public void write(int c) throws IOException; // Writer
Reader.read() 同样返回 int,同样用 -1 表示末尾。区别在于:这个 int 的低 16 位装的是一个 char,而不是一个 byte。
对应到文件读写,实现类是 FileReader 和 FileWriter。
第一个字符流示例
import java.io.*;
import java.nio.charset.StandardCharsets;
public class CharDemo {
public static void main(String[] args) throws IOException {
try (Writer writer = new FileWriter("poem.txt", StandardCharsets.UTF_8)) {
writer.write("床前明月光\n");
writer.write("疑是地上霜\n");
}
try (Reader reader = new FileReader("poem.txt", StandardCharsets.UTF_8)) {
int c;
while ((c = reader.read()) != -1) {
System.out.print((char) c);
}
}
}
}
运行:
javac CharDemo.java
java CharDemo
输出:
床前明月光
疑是地上霜
注意 write(String) 这个重载——Writer 可以直接写字符串,不用先转成数组,这比字节流方便太多。
一定要显式指定字符集
上面代码里的 StandardCharsets.UTF_8 不是可有可无的装饰。
FileReader 和 FileWriter 如果不传字符集,会使用运行环境的默认字符集。这个默认值在不同机器上不一样:Linux 和 macOS 通常是 UTF-8,中文 Windows 历史上长期是 GBK。
结果就是:你在自己电脑上跑得好好的程序,换台机器就乱码。
Warning「本机正常、别人电脑乱码」几乎百分之百是没指定字符集。养成习惯:只要涉及文本读写,永远显式写上字符集。
Note带字符集参数的
FileReader(File, Charset)和FileWriter(File, Charset)构造方法是 Java 11 引入的。Java 11 之前只能用后面第 88 章讲的InputStreamReader来指定编码。
从 Java 18 起,file.encoding 的默认值已统一为 UTF-8,情况好了很多。但显式指定依然是最稳妥的做法。
用缓冲区批量读
跟字节流一样,逐字符读效率低。Reader 提供了 read(char[] cbuf):
import java.io.*;
import java.nio.charset.StandardCharsets;
public class CharBuffer {
public static void main(String[] args) throws IOException {
try (Writer w = new FileWriter("article.txt", StandardCharsets.UTF_8)) {
for (int i = 0; i < 500; i++) {
w.write("Java 字符流练习。");
}
}
try (Reader reader = new FileReader("article.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[256];
int n;
int total = 0;
while ((n = reader.read(buffer)) != -1) {
total += n; // n 是本次读到的字符数
}
System.out.println("共读取 " + total + " 个字符");
}
}
}
输出:
共读取 5000 个字符
500 次循环,每次 10 个字符,正好 5000。注意这里统计的是字符数不是字节数——同样的文件用字节流读,会得到大得多的数字。
字符流与字节流对照
把两套体系放在一起比,差异一目了然:
| 对比项 | 字节流 | 字符流 |
|---|---|---|
| 基类 | InputStream / OutputStream | Reader / Writer |
| 最小单位 | 字节(8 位) | 字符(16 位) |
| 缓冲数组 | byte[] | char[] |
| 编码转换 | 不做,原样搬运 | 自动完成 |
| 适用数据 | 图片、视频、压缩包 | 文本文件、源码、配置 |
| 直接写字符串 | 不支持 | 支持 write(String) |
选型规则依然是那句话:能用文本编辑器打开看懂的,用字符流;打开是乱码的,用字节流。
Writer 的追加与刷新
FileWriter 也支持追加模式,参数位置在字符集之后:
// 追加写入,不覆盖原内容
try (Writer w = new FileWriter("log.txt", StandardCharsets.UTF_8, true)) {
w.write("新的一行\n");
}
Writer 同样有 flush() 方法。字符流内部为了做编码转换,天然带一小块缓冲,不 flush 也不 close 的话数据可能停在内存里。
用 try-with-resources 就不用操心这个问题,离开代码块自动关闭并刷新。
内存中的字符流
除了文件,字符流也有内存版实现。
StringReader 把一个字符串包装成 Reader,CharArrayWriter 把写入的字符收集到内存数组:
import java.io.*;
public class MemoryChar {
public static void main(String[] args) throws IOException {
String source = "内存里的文本";
try (Reader reader = new StringReader(source);
CharArrayWriter writer = new CharArrayWriter()) {
int c;
while ((c = reader.read()) != -1) {
writer.write(c);
}
System.out.println(writer.toString());
}
}
}
输出:
内存里的文本
这类内存流在测试中很有价值。你写的解析方法如果接收 Reader 参数,测试时喂个 StringReader 就行,不必准备真实文件。
一个常见误区
有人觉得字符流「更高级」,于是拿它读图片。这会出大问题。
字符流会尝试把字节按编码规则解释成字符。图片的字节序列不符合任何文本编码,非法字节会被替换成占位符 \uFFFD。这个替换是不可逆的——写回去的文件已经损坏了。
Warning二进制文件绝对不能过字符流。复制图片、视频、
.class文件,只能用字节流。
编码不一致会怎样
编码问题是新手最容易踩的坑,值得单独演示一次。
同一段中文,用 UTF-8 写出去,再用 GBK 读回来,结果一定是乱的:
import java.io.*;
import java.nio.charset.*;
public class CharsetMismatch {
public static void main(String[] args) throws IOException {
try (Writer w = new FileWriter("note.txt", StandardCharsets.UTF_8)) {
w.write("中文测试");
}
// 故意用错误的字符集读取
try (Reader r = new FileReader("note.txt", Charset.forName("GBK"))) {
char[] buf = new char[64];
int n = r.read(buf);
System.out.println("GBK 读出:" + new String(buf, 0, n));
}
try (Reader r = new FileReader("note.txt", StandardCharsets.UTF_8)) {
char[] buf = new char[64];
int n = r.read(buf);
System.out.println("UTF-8 读出:" + new String(buf, 0, n));
}
}
}
输出大致是这样:
GBK 读出:涓枃娴嬭瘯
UTF-8 读出:中文测试
文件本身没有任何问题,出错的只是解读方式。这就像同一串摩斯电码,用错了码表自然翻译不出原话。
Note文本文件不会记录自己是什么编码(BOM 是个不完全可靠的例外)。编码信息只存在于「约定」里,所以代码中必须写死。团队里统一用 UTF-8,能省掉九成的乱码事故。
字符与字节的数量关系
还有个细节容易混淆:字符数不等于字节数。
ASCII 字符在 UTF-8 下占 1 个字节,常用汉字占 3 个字节,部分生僻字和 emoji 占 4 个字节。所以 "abc中文" 是 5 个字符,却占 9 个字节。
用字符流读到的 n 是字符个数,用字节流读到的 n 是字节个数,两者不能互相换算。做长度校验、进度条、分片上传时,务必想清楚统计的到底是哪一个。
本节小结
- 字符流是「字节流 + 编码转换」,专门处理文本。
Reader.read()返回的int里装的是char,-1仍表示结束。- 读写文本一律显式传
StandardCharsets.UTF_8,否则换机器就乱码。 FileReader/FileWriter的字符集参数从 Java 11 才有。- 批量读用
char[],统计的是字符数不是字节数。 - 二进制文件只能用字节流,过字符流必定损坏。