字符输入流与字符输出流 | JavaSE

字符输入流与字符输出流

一、学习目标

学完本章,你应该能够:

  1. 能够解释 FileReaderFileWriter 的作用和继承关系。
  2. 能够使用 read()read(char[]) 读取文本字符。
  3. 能够解释字符流为什么可以正确处理跨字节边界的文本字符。
  4. 能够使用 write(int)write(char[])write(String) 等方式写文本。
  5. 能够区分字符输出流的覆盖模式与追加模式。
  6. 能够理解 flush()close() 的基本差异。
  7. 能够根据 JDK 21 API 显式为 FileReader / FileWriter 指定 Charset
  8. 能够解释默认字符集与文件实际字符集不匹配为什么仍会乱码。
  9. 能够判断什么时候选择字节流,什么时候选择字符流。
  10. 能够明确本章与后续:
  • 缓冲流
  • 转换流
  • 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 知识问答

  1. FileReader 属于什么体系?
  2. FileWriter 属于什么体系?
  3. FileReader 在 JDK 21 中直接继承谁?
  4. FileWriter 直接继承谁?
  5. read() 返回什么?
  6. EOF 用什么值表示?
  7. read(char[]) 返回什么?
  8. 为什么字符数组读取仍然需要 len
  9. 字符流为什么可以正确处理 UTF-8 中文字节边界?
  10. Writer.write(int)OutputStream.write(int) 有什么本质差异?
  11. FileWriter 可以直接写 String 吗?
  12. FileWriter 默认覆盖还是追加?
  13. JDK 21 如何直接为 FileReader 指定 UTF-8?
  14. JDK 21 如何为 FileWriter 指定 UTF-8?
  15. 字符流是否永远不会乱码?
  16. flush()close() 有什么区别?
  17. 为什么 FileReader 不适合图片?
  18. 为什么转换流在 JDK 21 仍然有学习价值?
  19. 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
            )
    );
}

回答:

  1. buffer 保存 byte 还是 char?
  2. len 表示什么?
  3. 为什么不能直接:
new String(buffer)
  1. 字节到字符的解码发生在哪里?
  2. 如果 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

回答:

  1. FileReader 能否正常创建?
  2. 为什么创建成功也可能读取乱码?
  3. Bug 是“流类型错误”还是“Charset 协议错误”?
  4. 如何修复?

另一个错误:

char[] buffer =
        new char[1024];

int len;

while (
        (len = reader.read(buffer))
                != -1
) {

    System.out.print(
            new String(buffer)
    );
}

解释:

最后一次读取为什么可能输出历史残留字符。


7.5 综合训练

编写:

TextFileProcessor

功能:

  1. 以 UTF-8 读取源文本;
  2. 统计字符数组实际读取总量;
  3. 把所有 "Java" 替换为 "JavaSE"
  4. 写入新的 UTF-8 文件;
  5. 目标文件存在时覆盖;
  6. 再实现追加模式;
  7. 使用:
    • FileReader
    • FileWriter
  8. 不使用:
    • 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 的关系。