字符输入流与字符输出流 | JavaSE
字符输入流与字符输出流
一、学习目标
学完本章,你应该能够:
- 能够解释
FileReader与FileWriter的作用和继承关系。 - 能够使用
read()和read(char[])读取文本字符。 - 能够解释字符流为什么可以正确处理跨字节边界的文本字符。
- 能够使用
write(int)、write(char[])、write(String)等方式写文本。 - 能够区分字符输出流的覆盖模式与追加模式。
- 能够理解
flush()和close()的基本差异。 - 能够根据 JDK 21 API 显式为
FileReader/FileWriter指定Charset。 - 能够解释默认字符集与文件实际字符集不匹配为什么仍会乱码。
- 能够判断什么时候选择字节流,什么时候选择字符流。
- 能够明确本章与后续:
- 缓冲流
- 转换流
- try-with-resources 的章节边界。
二、核心知识
2.1 为什么已经有字节流还需要字符流
上一章我们已经知道:
磁盘文件
最终都是 byte
那么为什么还需要:
Reader
Writer
?
因为:
文本处理不仅关心 byte,还关心这些 byte 按照字符集解码以后代表什么字符。
例如 UTF-8:
中
通常由:
3 个 byte
编码。
如果我们自己使用:
FileInputStream
分批读字节,
还必须负责:
字节边界
+
字符集解码
+
多字节字符拼接
而字符输入流把这层复杂性封装起来:
文件 byte
↓
解码器
↓
character
↓
Reader.read()
所以:
字节流关注原始字节;字符流关注文本字符。
2.2 FileReader
FileReader:
java.io.FileReader
在 JDK 21 中继承关系:
Reader
↑
InputStreamReader
↑
FileReader
因此:
FileReader
属于:
字符输入流体系。
作用:
从字符文件读取文本,并把文件字节按照指定字符集或默认字符集解码成字符。
数据链路:
文本文件
│
│ byte
▼
FileReader
│
│ 解码
▼
character
│
▼
Java
2.3 FileReader 的基础构造器
传统写法:
new FileReader(String fileName)
以及:
new FileReader(File file)
例如:
Reader reader =
new FileReader(
"data/article.txt"
);
这种构造器使用:
平台默认字符集。
因此:
文件实际编码
=
默认 Charset
时才能正确读取文本。
2.4 JDK 21 可以直接指定 Charset
现代 JDK 还支持:
new FileReader(
String fileName,
Charset charset
)
以及:
new FileReader(
File file,
Charset charset
)
例如:
Reader reader =
new FileReader(
"data/article.txt",
StandardCharsets.UTF_8
);
这明确表达:
该文件应该按照 UTF-8 解码。
因此在 JDK 21 项目中,如果你明确知道文件编码:
StandardCharsets.UTF_8
通常比完全依赖:
默认 Charset
更加清晰可靠。
2.5 FileReader.read()
方法:
int read()
表示:
每次读取一个字符数据。
例如文件:
ABC
调用:
int ch = reader.read();
依次可能得到:
65
66
67
-1
其中:
-1
仍然表示:
已经到达字符流末尾。
2.6 为什么字符 read() 仍然返回 int
和字节流类似,
需要同时表示:
有效数据
和:
-1
这个 EOF 状态。
所以:
int ch;
然后:
while (
(ch = reader.read())
!= -1
) {
System.out.print(
(char) ch
);
}
2.7 字符输入流为什么可以正确读中文
假设文件 UTF-8:
A中B
底层字节类似:
A
→ 1 byte
中
→ 3 bytes
B
→ 1 byte
字符流内部会完成:
byte
↓
Charset Decoder
↓
Unicode character
所以调用:
reader.read()
从使用者角度得到的是:
A
中
B
而不是:
A
中 的第 1 个 byte
中 的第 2 个 byte
中 的第 3 个 byte
B
这就是字符流处理文本的核心价值。
2.8 read(char[])
更常用:
int read(char[] cbuf)
例如:
char[] buffer =
new char[1024];
int len =
reader.read(buffer);
这里:
buffer
→ 最多能装多少 char
len
→ 本轮实际上读取多少 char
依然必须区分:
数组容量
与:
实际有效数据长度
2.9 标准字符数组读取
char[] buffer =
new char[1024];
int len;
while (
(len = reader.read(buffer))
!= -1
) {
String text =
new String(
buffer,
0,
len
);
System.out.print(text);
}
与上一章字节数组相比:
byte[]
→ 原始字节
char[]
→ 字符数据
这时:
new String(
buffer,
0,
len
)
是在:
从已经解码后的字符数组构造字符串。
不会再自己处理 UTF-8 字节边界。
2.10 FileWriter
FileWriter:
java.io.FileWriter
JDK 21 继承关系:
Writer
↑
OutputStreamWriter
↑
FileWriter
作用:
把 Java 中的字符数据编码成字节,然后写入字符文件。
数据链路:
Java character
│
│ FileWriter
│ 编码
▼
byte
│
▼
文本文件
因此:
FileReader
=
文件 byte
→ character
FileWriter
=
character
→ 文件 byte
2.11 FileWriter 的默认构造器
例如:
Writer writer =
new FileWriter(
"data/result.txt"
);
默认使用:
平台默认 Charset
并且:
默认覆盖原文件内容
2.12 FileWriter 追加模式
使用:
new FileWriter(
"data/result.txt",
true
);
表示:
append = true
从文件末尾继续写。
因此:
默认
→ 覆盖
true
→ 追加
和:
FileOutputStream
非常相似。
2.13 JDK 21 指定 Charset
JDK 21 支持:
new FileWriter(
String fileName,
Charset charset
)
以及:
new FileWriter(
String fileName,
Charset charset,
boolean append
)
例如:
Writer writer =
new FileWriter(
"data/result.txt",
StandardCharsets.UTF_8
);
追加 UTF-8:
Writer writer =
new FileWriter(
"data/result.txt",
StandardCharsets.UTF_8,
true
);
这样代码明确说明:
目标文本文件编码
=
UTF-8
三、使用方法
3.1 FileReader 逐字符读取
import java.io.FileReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
public class FileReaderDemo {
public static void main(String[] args)
throws Exception {
try (
Reader reader =
new FileReader(
"data/article.txt",
StandardCharsets.UTF_8
)
) {
int ch;
while (
(ch = reader.read())
!= -1
) {
System.out.print(
(char) ch
);
}
}
}
}
如果:
article.txt
真实编码是 UTF-8,
中文即可按照 UTF-8 正确解码。
3.2 字符数组读取
更常见:
import java.io.FileReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
public class FileReaderBufferDemo {
public static void main(String[] args)
throws Exception {
try (
Reader reader =
new FileReader(
"data/article.txt",
StandardCharsets.UTF_8
)
) {
char[] buffer =
new char[1024];
int len;
while (
(len = reader.read(buffer))
!= -1
) {
System.out.print(
new String(
buffer,
0,
len
)
);
}
}
}
}
这时:
读取单位
=
char
而不是:
byte
3.3 FileWriter.write(int)
Writer 中:
void write(int c)
表示:
写出一个字符对应的 16 位值。
例如:
writer.write('A');
writer.write('中');
这里与:
OutputStream.write(int)
非常容易混淆。
必须区分:
OutputStream.write(int)
→ 只写最低 8 位
→ 一个 byte
Writer.write(int)
→ 写一个字符
→ 16 位 char 语义
3.4 write(char[])
可以:
char[] chars =
{'J', 'a', 'v', 'a'};
writer.write(chars);
写入:
Java
3.5 write(char[], off, len)
例如:
char[] chars =
{'A', 'B', 'C', 'D', 'E'};
writer.write(
chars,
1,
3
);
写出:
BCD
3.6 write(String)
这是字符输出流非常方便的一点:
writer.write(
"星雨笔录 JavaSE 教程"
);
不需要我们先:
String.getBytes(...)
因为:
Writer
会负责:
character
↓
Charset Encoder
↓
byte
3.7 write(String, off, len)
例如:
String text =
"ABCDE";
writer.write(
text,
1,
3
);
写入:
BCD
3.8 写多行文本
当前可以:
writer.write("第一行");
writer.write(
System.lineSeparator()
);
writer.write("第二行");
writer.write(
System.lineSeparator()
);
writer.write("第三行");
后面学习:
BufferedWriter
之后会得到:
newLine()
这种更加专门的换行能力。
因此本章暂时不提前展开。
3.9 完整 UTF-8 文本写出
import java.io.FileWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
public class FileWriterDemo {
public static void main(String[] args)
throws Exception {
try (
Writer writer =
new FileWriter(
"data/result.txt",
StandardCharsets.UTF_8
)
) {
writer.write(
"星雨笔录"
);
writer.write(
System.lineSeparator()
);
writer.write(
"JavaSE File 与 IO"
);
}
}
}
数据过程:
Java String
↓
character
↓
UTF-8 编码
↓
byte
↓
result.txt
3.10 flush()
Writer 提供:
flush()
作用:
请求把流内部暂存、缓冲的字符尽快向目标继续写出。
例如:
writer.write("Hello");
writer.flush();
flush() 后:
流仍然可以继续使用
所以可以:
writer.write("Java");
3.11 close()
close():
关闭流并释放资源。
对于 Writer:
close()
还会在关闭前完成必要的刷新。
关闭后:
writer.write(...)
通常就不能继续进行。
因此:
flush
→ 刷新
→ 流仍然可用
close
→ 刷新 + 关闭
→ 流不可继续使用
但资源管理策略将在:
07-13
系统讲解。
四、原理与进阶
4.1 FileReader 本质上已经是转换流体系的一种具体类
观察继承:
Reader
↑
InputStreamReader
↑
FileReader
这意味着:
FileReader
本身已经建立在:
InputStreamReader
的字节→字符转换能力之上。
逻辑:
文件 byte
↓
InputStreamReader 的解码机制
↓
character
↓
FileReader API
4.2 FileWriter 同理
继承:
Writer
↑
OutputStreamWriter
↑
FileWriter
本质:
character
↓
OutputStreamWriter 编码
↓
文件 byte
所以:
字符流并没有让磁盘突然开始保存 char。
磁盘仍然是:
byte
只是 Java 在中间替我们自动进行了:
编码 / 解码
4.3 那么 07-11 转换流还有什么意义
这是非常好的问题。
JDK 21 中:
new FileReader(
file,
charset
);
已经可以直接指定编码。
那么:
InputStreamReader
为什么还要学?
因为:
FileReader
只针对:
文件。
而:
InputStreamReader
可以包装:
任意 InputStream
例如未来:
网络 InputStream
System.in
ByteArrayInputStream
Socket InputStream
其他字节流
统一转换成:
Reader
所以:
FileReader
=
文件场景便利实现
InputStreamReader
=
通用 byte → character 桥梁
同理:
FileWriter
=
文件字符输出便利类
OutputStreamWriter
=
通用 character → byte 桥梁
这就是 07-11 仍然独立存在的原因。
4.4 默认字符集仍然可能制造乱码
假设:
article.txt
=
GBK
但是:
new FileReader(
"article.txt",
StandardCharsets.UTF_8
);
会发生:
GBK bytes
↓
UTF-8 Decoder
↓
错误字符
所以:
字符流不是“永不乱码流”。
字符流只是:
帮你进行字符编码/解码。
真正是否正确仍取决于:
实际文件编码
=
Reader 使用 Charset
4.5 FileWriter 同样必须考虑目标编码
假设你:
new FileWriter(
file,
StandardCharsets.UTF_8
)
写出的文件就是:
UTF-8 字节
以后别的软件如果:
按照 GBK 打开
依然可能乱码。
因此字符编码永远是:
写入协议
+
读取协议
双方协作的问题。
4.6 Reader 的 char 与 Unicode 码点
这里还需要结合 07-05。
Java:
char
=
16 位 UTF-16 代码单元
因此:
reader.read()
所谓“一次读取一个字符”,从 Java API 数据单位角度更准确地说是:
读取一个
char/ UTF-16 代码单元。
对于:
绝大多数常见汉字
英文
BMP 字符
一个 char 就可以表示。
但是补充 Unicode 字符,例如某些:
Emoji
生僻汉字
可能需要:
两个 char
即代理对(Surrogate Pair)。
所以不要把:
Reader.read()
绝对理解成:
一次必然得到一个完整的人类视觉字符。
4.7 为什么字符流不能复制二进制文件
假设:
photo.jpg
然后:
FileReader
↓
把 JPEG byte 当文本解码
↓
char
↓
FileWriter
↓
重新编码
这中间发生:
byte
→ character
→ byte
如果原始 byte 并不是有效文本编码,
解码可能:
丢失 / 替换
原始字节就无法保持一致。
结果:
文件损坏
所以:
二进制复制
→ byte stream
文本语义处理
→ character stream
五、实践应用
5.1 Markdown 读取
例如:
JavaSE.md
需要:
分析正文
统计字符
查找关键词
解析 Markdown
这类需求本质关心:
文本内容
应该优先:
Reader
而不是自己处理 UTF-8 byte。
5.2 日志写出
例如:
2026-09-03 login success
本质是:
文本
使用:
Writer
非常自然。
后续进一步:
BufferedWriter
PrintWriter
日志框架
会提供更强的文本输出能力。
5.3 配置文件
例如:
application.properties
config.json
data.csv
如果需要按照:
字符
行
字符串
处理,
字符流更加符合业务语义。
5.4 字节流还是字符流判断口诀
不要死记后缀,可以问:
我的业务关心的是原始 byte,还是文本 character?
如果:
必须保持原始字节完全一致
使用:
字节流
例如:
图片复制
视频复制
ZIP
PDF
附件传输
如果:
我要理解和处理文字内容
使用:
字符流
例如:
读取 Java 代码
读取 Markdown
写日志
处理 CSV 文本
六、常见问题
6.1 FileReader 是直接从磁盘读取 char 吗?
高层使用角度可以理解为:
读取字符
但底层:
磁盘保存 byte
↓
FileReader 解码
↓
char
6.2 FileWriter 写入磁盘的是 char 吗?
不是。
最终仍然:
character
↓
Charset 编码
↓
byte
↓
磁盘
6.3 FileReader 会自动解决所有乱码吗?
不会。
必须保证:
文件实际编码
=
读取使用 Charset
6.4 read(char[]) 为什么仍然需要 len?
和 byte 数组完全一样。
数组:
容量固定
每次实际读取:
不一定填满
所以:
new String(
buffer,
0,
len
);
6.5 FileWriter 默认覆盖还是追加?
默认:
覆盖
追加:
new FileWriter(
path,
true
);
或者 JDK 21 指定字符集:
new FileWriter(
path,
charset,
true
);
6.6 flush 和 close 一样吗?
不一样:
flush
→ 将待输出数据继续推出
→ 流仍可使用
close
→ 完成关闭
→ 释放资源
→ 不能继续正常使用
6.7 为什么有 FileReader(Charset),还要学习转换流?
因为:
FileReader
只能方便地解决:
文件
而:
InputStreamReader
可以包装任意:
InputStream
因此更加通用。
6.8 字符流能复制 txt 吗?
技术上:
可以
只要:
源编码
读取 Charset
目标编码
写入 Charset
都处理正确。
但如果任务只是:
原样复制文件,一个字节都不要改变。
那么即使是 TXT:
字节流
同样非常合适,而且可以确保:
byte-for-byte
原样复制。
字符流更适合:
需要理解、修改文本内容。
七、练习与验收
7.1 知识问答
FileReader属于什么体系?FileWriter属于什么体系?FileReader在 JDK 21 中直接继承谁?FileWriter直接继承谁?read()返回什么?- EOF 用什么值表示?
read(char[])返回什么?- 为什么字符数组读取仍然需要
len? - 字符流为什么可以正确处理 UTF-8 中文字节边界?
Writer.write(int)与OutputStream.write(int)有什么本质差异?- FileWriter 可以直接写 String 吗?
- FileWriter 默认覆盖还是追加?
- JDK 21 如何直接为 FileReader 指定 UTF-8?
- JDK 21 如何为 FileWriter 指定 UTF-8?
- 字符流是否永远不会乱码?
flush()与close()有什么区别?- 为什么 FileReader 不适合图片?
- 为什么转换流在 JDK 21 仍然有学习价值?
- Reader 中“一次一个字符”为什么不一定等于一个完整 Unicode 码点?
7.2 代码阅读
char[] buffer =
new char[1024];
int len;
while (
(len = reader.read(buffer))
!= -1
) {
System.out.print(
new String(
buffer,
0,
len
)
);
}
回答:
buffer保存 byte 还是 char?len表示什么?- 为什么不能直接:
new String(buffer)
- 字节到字符的解码发生在哪里?
- 如果 Reader 使用错误 Charset,还会不会乱码?
7.3 手写代码
任务一:读取 UTF-8 文本
使用:
FileReader
+
StandardCharsets.UTF_8
读取:
JavaSE学习笔记.txt
要求:
字符数组批量读取
并完整输出。
任务二:写学生信息
使用:
FileWriter
写:
姓名:张三
年龄:20
专业:软件工程
要求:
UTF-8
并正确换行。
任务三:追加日志
第一次:
程序启动
第二次运行追加:
用户登录
要求:
不能覆盖第一行。
任务四:编码实验
创建一个:
UTF-8 文件
分别使用:
UTF-8
和另一个不匹配 Charset 读取,
观察结果并解释。
7.4 Debug
错误:
Reader reader =
new FileReader(
"utf8.txt",
Charset.forName("GBK")
);
已知:
utf8.txt
真实编码:
UTF-8
回答:
- FileReader 能否正常创建?
- 为什么创建成功也可能读取乱码?
- Bug 是“流类型错误”还是“Charset 协议错误”?
- 如何修复?
另一个错误:
char[] buffer =
new char[1024];
int len;
while (
(len = reader.read(buffer))
!= -1
) {
System.out.print(
new String(buffer)
);
}
解释:
最后一次读取为什么可能输出历史残留字符。
7.5 综合训练
编写:
TextFileProcessor
功能:
- 以 UTF-8 读取源文本;
- 统计字符数组实际读取总量;
- 把所有
"Java"替换为"JavaSE"; - 写入新的 UTF-8 文件;
- 目标文件存在时覆盖;
- 再实现追加模式;
- 使用:
- FileReader
- FileWriter
- 不使用:
- BufferedReader
- BufferedWriter
- Files.readString
- Files.writeString
目的是:
只练习本章字符流基本功。
7.6 本章验收
必须能够闭卷解释:
FileReader
=
文件字符输入流
磁盘 byte
↓
Charset 解码
↓
char
↓
Java
FileWriter
=
文件字符输出流
Java char
↓
Charset 编码
↓
byte
↓
磁盘
Reader
适合读取文本字符。
Writer
适合写出文本字符。
但字符流是否乱码,
仍取决于:
真实文本编码
=
使用的 Charset。
JDK 21 的 FileReader / FileWriter
已经支持直接指定 Charset。
字节流适合原始 byte,
尤其是任意二进制数据。
字符流适合需要理解文本语义的场景。
最终:
- [ ] 能独立使用 FileReader。
- [ ] 能独立使用 FileWriter。
- [ ] 能使用
read(char[])。 - [ ] 能正确处理
len。 - [ ] 能使用
write(String)。 - [ ] 能实现覆盖和追加。
- [ ] 能正确指定 UTF-8。
- [ ] 能解释编码不匹配乱码。
- [ ] 能区分 byte stream 与 character stream。
- [ ] 能解释 FileReader 与 InputStreamReader 的关系。
- [ ] 能解释 FileWriter 与 OutputStreamWriter 的关系。