字符流
字符流用于处理文本数据。和字节流不同,字符流关心的是字符,而不是原始字节。只要涉及中文、英文、符号、换行、配置文件、日志、JSON、XML 等文本内容,就应该优先考虑字符流。
# 1. 为什么需要字符流
计算机底层存储的是字节,但人阅读的是字符。字符和字节之间需要通过字符集转换。
例如字符串 "中国":
- 使用 UTF-8 编码通常占 6 个字节。
- 使用 GBK 编码通常占 4 个字节。
如果直接用字节流逐字节处理文本,很容易在多字节字符中间截断,导致乱码。字符流会帮我们完成字节到字符的解码,或者字符到字节的编码。
# 2. 字符流的核心类
字符流的两个顶层抽象类是:
Reader:字符输入流。Writer:字符输出流。
常用方法如下:
int read()
int read(char[] cbuf)
int read(char[] cbuf, int off, int len)
void write(int c)
void write(char[] cbuf)
void write(String str)
void flush()
void close()
# 3. 读取文本文件
推荐使用 Files.newBufferedReader 并显式指定字符集。
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadTextDemo {
public static void main(String[] args) throws IOException {
Path path = Path.of("application.log");
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
readLine() 不会把行尾换行符返回。如果写出时需要保留换行,需要自己追加。
# 4. 写入文本文件
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class WriteTextDemo {
public static void main(String[] args) throws IOException {
Path path = Path.of("result.txt");
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("第一行");
writer.newLine();
writer.write("第二行");
}
}
}
newLine() 会写入当前系统的换行符。如果需要跨平台固定输出格式,可以直接写 \n。
# 5. FileReader 和 FileWriter 的注意点
FileReader 和 FileWriter 使用起来简单:
try (Reader reader = new FileReader("a.txt")) {
// read text
}
但它们的主要问题是字符集不够直观。旧版本 Java 中默认字符集依赖操作系统环境,容易出现“本地正常、服务器乱码”的问题。Java 18 起默认字符集统一为 UTF-8,但生产代码仍建议显式指定字符集,让代码意图更清晰。
推荐:
Files.newBufferedReader(path, StandardCharsets.UTF_8);
Files.newBufferedWriter(path, StandardCharsets.UTF_8);
# 6. 字符流适合什么场景
适合使用字符流的场景:
- 读取配置文件。
- 读取 CSV、JSON、XML 等文本文件。
- 处理日志文件。
- 生成 SQL、HTML、Markdown 等文本。
- 需要按行读取的场景。
不适合使用字符流的场景:
- 图片、视频、音频。
- 压缩包。
- 加密后的字节。
- 任意二进制协议。
# 7. 字符流与编码的关系
字符流不是完全脱离字节流存在的。真正读写文件时,底层仍然是字节:
读取文本:字节 -> 按字符集解码 -> 字符
写入文本:字符 -> 按字符集编码 -> 字节
所以字符流的关键不是“比字节流高级”,而是“封装了字符集转换”。
# 专家实践清单
| 场景 | 推荐 API | 关键点 |
|---|---|---|
| 读取 UTF-8 文本 | Files.newBufferedReader | 显式指定 StandardCharsets.UTF_8 |
| 写入文本 | Files.newBufferedWriter | 明确覆盖、追加和创建策略 |
| 按行处理日志 | BufferedReader.readLine | 注意行尾换行符会被去掉 |
| 编码转换 | InputStreamReader + OutputStreamWriter | 源编码和目标编码都要明确 |
字符流背后的转换链路:
磁盘/网络字节
│ CharsetDecoder
▼
Java char / String
│ 业务处理
▼
Java char / String
│ CharsetEncoder
▼
磁盘/网络字节
# Tips 快问快答
Q:字符流适合什么场景? A:适合处理文本,例如配置、日志、CSV、模板文件。
Q:字符流是否不需要关心编码? A:需要。字符流只是封装了编码/解码过程,仍然必须指定字符集。
Q:为什么不建议直接用 FileReader?
A:它使用平台默认字符集,跨环境容易乱码。
Q:BufferedReader.readLine() 会保留换行符吗?
A:不会,返回内容不包含行尾换行符。
Q:文本文件能不能用字节流读? A:可以,但需要手动把字节按正确字符集解码为字符。
Q:乱码的本质是什么? A:写入时编码和读取时解码使用的字符集不一致。
Q:生产环境推荐统一什么编码? A:通常统一 UTF-8,并在代码、配置、数据库和传输层显式声明。
Q:字符流最大的边界是什么? A:它不适合处理任意二进制数据,否则可能破坏原始字节。
# 8. 总结
字符流专门用于文本处理。只要处理的是人可读文本,就应该优先使用字符流,并显式指定字符集。生产代码中最常见、最稳妥的组合是 Files.newBufferedReader、Files.newBufferedWriter 和 StandardCharsets.UTF_8。