转换流 | JavaSE
转换流
一、学习目标
完成本章后,你应该能够:
- 能够解释为什么字节流和字符流之间需要转换。
- 能够理解
InputStreamReader的作用:字节 → 字符。 - 能够理解
OutputStreamWriter的作用:字符 → 字节。 - 能够指定正确字符集读取文本,解决编码不匹配导致的乱码问题。
- 能够指定字符集写出文本。
- 能够组合
FileInputStream、InputStreamReader、BufferedReader等流形成完整 IO 管道。 - 能够理解 JDK 21 中转换流与
FileReader/FileWriter + Charset的关系。
二、核心知识
2.1 先回忆一个最重要的问题:文本文件本质是什么
假设文件中有:
你好Java
磁盘里真正保存的不是:
char
而是:
byte
也就是说:
字符
↓ 编码
字节
↓
文件
读取时反过来:
文件
↓
字节
↓ 解码
字符
因此文本 IO 的本质离不开两个过程:
编码:char/String → byte
解码:byte → char/String
2.2 什么是转换流
Java 提供两个非常经典的转换流:
InputStreamReader
OutputStreamWriter
它们分别负责:
InputStreamReader
byte → char
解码
OutputStreamWriter
char → byte
编码
整个关系:
解码
InputStream(byte) ───────→ Reader(char)
InputStreamReader
编码
Writer(char) ─────────────→ OutputStream(byte)
OutputStreamWriter
因此转换流可以理解为:
字节流与字符流之间的桥梁。
2.3 InputStreamReader
InputStreamReader:
public class InputStreamReader extends Reader
所以它本质属于:
字符输入流
但是它需要接收:
InputStream
然后把底层读取到的:
byte
按照指定字符集解码为:
char
结构:
文件 / 网络 / 其他字节源
↓
InputStream
↓
InputStreamReader
↓
Reader
↓
字符
2.4 OutputStreamWriter
OutputStreamWriter:
public class OutputStreamWriter extends Writer
所以它属于:
字符输出流
它接收字符:
char / String
按照指定字符集编码成:
byte
然后交给底层:
OutputStream
结构:
字符
↓
OutputStreamWriter
↓
OutputStream
↓
文件 / 网络 / 其他字节目标
2.5 为什么会乱码
假设文件原来采用:
GBK
保存:
中国
磁盘中存在的是按照 GBK 规则生成的字节。
如果读取时却说:
这些字节按照 UTF-8 解码
那么就发生:
GBK 编码
↓
GBK 字节
↓
UTF-8 解码
↓
错误字符 / 乱码
所以真正必须记住的规则是:
编码时使用什么字符集,解码时原则上就必须按照对应字符集解释这些字节。
例如:
UTF-8 编码 → UTF-8 解码
GBK 编码 → GBK 解码
而不是:
GBK 编码 → UTF-8 解码
三、使用方法
3.1 InputStreamReader 默认字符集
最基本构造器:
InputStreamReader(InputStream in)
例如:
InputStreamReader reader =
new InputStreamReader(
new FileInputStream("data.txt")
);
这里:
FileInputStream
负责:
读取 byte
而:
InputStreamReader
负责:
byte → char
如果没有指定字符集:
new InputStreamReader(inputStream)
就使用 Java 默认字符集。
3.2 指定字符集
更加重要的形式:
InputStreamReader(
InputStream in,
Charset charset
)
JDK 21 中建议优先使用:
Charset
而不是手写字符集字符串。
例如读取 UTF-8:
import java.io.*;
import java.nio.charset.StandardCharsets;
public class InputStreamReaderDemo {
public static void main(String[] args) throws IOException {
try (
InputStreamReader reader =
new InputStreamReader(
new FileInputStream("data.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)
);
}
}
}
}
注意:
StandardCharsets.UTF_8
比:
"UTF-8"
更加类型安全,也避免字符集名称写错。
3.3 读取 GBK 文件
StandardCharsets 中没有:
GBK
常量。
可以使用:
Charset.forName("GBK")
例如:
import java.io.*;
import java.nio.charset.Charset;
public class ReadGbkDemo {
public static void main(String[] args) throws IOException {
Charset gbk = Charset.forName("GBK");
try (
InputStreamReader reader =
new InputStreamReader(
new FileInputStream("gbk.txt"),
gbk
)
) {
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer)) != -1) {
System.out.print(
new String(buffer, 0, len)
);
}
}
}
}
数据过程:
gbk.txt
↓
FileInputStream
↓
GBK 字节
↓
InputStreamReader
↓ 指定 GBK 解码
Unicode 字符
↓
Java 程序
只要:
文件真实编码 = 解码使用的字符集
文本就可以被正确解释。
3.4 InputStreamReader + BufferedReader
实际开发中,更常见的不是直接:
InputStreamReader.read()
而是继续包装:
BufferedReader
例如:
import java.io.*;
import java.nio.charset.Charset;
public class GbkBufferedReaderDemo {
public static void main(String[] args) throws IOException {
try (
BufferedReader br =
new BufferedReader(
new InputStreamReader(
new FileInputStream("gbk.txt"),
Charset.forName("GBK")
)
)
) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
}
现在出现三层流:
FileInputStream
↓
InputStreamReader
↓
BufferedReader
每一层职责都非常清晰。
FileInputStream
负责:
从文件读取字节
InputStreamReader
负责:
按照指定字符集
byte → char
BufferedReader
负责:
缓冲
+
readLine()
因此不要把下面这一串代码看成“套娃”:
new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"),
charset
)
);
应该读成:
我要从文件读 byte
↓
我要按指定编码把 byte 解成 char
↓
我要增加缓冲并按行读取
这样 Java IO 就会突然变得很好理解。
3.5 OutputStreamWriter 基础
OutputStreamWriter 用来:
把字符按照某种字符集编码为字节,然后写到底层字节流。
UTF-8 写出:
import java.io.*;
import java.nio.charset.StandardCharsets;
public class OutputStreamWriterDemo {
public static void main(String[] args) throws IOException {
try (
OutputStreamWriter writer =
new OutputStreamWriter(
new FileOutputStream("data.txt"),
StandardCharsets.UTF_8
)
) {
writer.write("你好 Java");
writer.write("\n");
writer.write("你好 IO");
}
}
}
数据过程:
"你好 Java"
↓
Java 字符
↓
OutputStreamWriter
↓ UTF-8 编码
byte
↓
FileOutputStream
↓
data.txt
3.6 使用 GBK 写出文本
import java.io.*;
import java.nio.charset.Charset;
public class WriteGbkDemo {
public static void main(String[] args) throws IOException {
Charset gbk = Charset.forName("GBK");
try (
Writer writer =
new OutputStreamWriter(
new FileOutputStream("gbk.txt"),
gbk
)
) {
writer.write("你好 Java");
}
}
}
此时:
Java 字符
↓
GBK 编码
↓
GBK 字节
↓
gbk.txt
如果之后读取:
GBK 解码
就能恢复正确字符。
3.7 OutputStreamWriter + BufferedWriter
和输入端一样,输出端也可以继续包装缓冲流:
BufferedWriter bw =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("result.txt"),
StandardCharsets.UTF_8
)
);
结构:
BufferedWriter
↓
OutputStreamWriter
↓
FileOutputStream
↓
文件
职责:
BufferedWriter
缓冲 + newLine()
OutputStreamWriter
char → byte,负责编码
FileOutputStream
把 byte 写入文件
例如:
import java.io.*;
import java.nio.charset.StandardCharsets;
public class BufferedUtf8WriterDemo {
public static void main(String[] args) throws IOException {
try (
BufferedWriter bw =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("students.txt"),
StandardCharsets.UTF_8
)
)
) {
bw.write("1001,张三,Java");
bw.newLine();
bw.write("1002,李四,MySQL");
bw.newLine();
bw.write("1003,王五,Spring Boot");
}
}
}
四、原理与进阶
4.1 转换流为什么叫“桥梁”
Java IO 最根本的两大数据体系:
InputStream / OutputStream
↓
byte
Reader / Writer
↓
char
两套体系中间怎么办?
就是:
InputStreamReader
OutputStreamWriter
所以可以画出完整关系:
解码
InputStream ─────────────────→ Reader
InputStreamReader
编码
Writer ─────────────────────→ OutputStream
OutputStreamWriter
一句话记忆:
Reader 方向的转换流负责解码,Writer 方向的转换流负责编码。
4.2 Java 内部字符与文件编码不要混为一谈
Java 程序里面:
String name = "中国";
你日常操作的是:
Java 字符 / String
但是一旦:
写入文件
最终就必须变成:
byte
因此需要:
编码
读取文件则反过来:
byte
↓
解码
↓
Java 字符
所以理解转换流以后,可以把字符 IO 看成:
字符输入:
外部 byte
→ 解码
→ Java char/String
字符输出:
Java char/String
→ 编码
→ 外部 byte
这才是字符流真正的本质。
4.3 默认字符集为什么不能乱依赖
如果写:
new InputStreamReader(inputStream)
就使用默认字符集。
在 JDK 21 环境中,Java 默认字符集通常按 UTF-8 统一。
但工程实践中,只要:
外部数据的编码是明确的
最好也明确写:
StandardCharsets.UTF_8
原因不是代码写不出来,而是:
让编码契约显式化。
别人看到:
new InputStreamReader(
in,
StandardCharsets.UTF_8
)
立刻知道:
这个输入必须按照 UTF-8 解码。
比隐式依赖默认环境更加清楚。
4.4 JDK 21 中 FileReader 已经可以指定 Charset
这里需要补充一个现代 Java 很重要的变化。
以前很多教程解决:
指定编码读取文件
时会重点写:
new InputStreamReader(
new FileInputStream("data.txt"),
StandardCharsets.UTF_8
);
这种写法当然仍然完全成立。
但是现代 JDK 中:
FileReader
已经提供:
FileReader(String fileName, Charset charset)
因此 JDK 21 可以直接:
Reader reader =
new FileReader(
"data.txt",
StandardCharsets.UTF_8
);
写文件也一样:
Writer writer =
new FileWriter(
"data.txt",
StandardCharsets.UTF_8
);
所以如果只是:
文件 + 指定字符集
可以直接:
FileReader + Charset
FileWriter + Charset
代码更简单。
那么转换流是不是没用了?
当然不是。
4.5 转换流真正无法替代的价值
InputStreamReader 接收的是:
InputStream
而不是:
FileInputStream
也就是说:
任何字节输入流
都可以转换成:
字符输入流
以后网络编程会看到:
Socket socket = ...;
InputStream inputStream =
socket.getInputStream();
如果网络上传输的是文本:
BufferedReader br =
new BufferedReader(
new InputStreamReader(
inputStream,
StandardCharsets.UTF_8
)
);
这时:
FileReader
就无能为力了。
因为数据来源根本不是文件。
所以转换流真正应该理解成:
通用 byte ↔ char 桥梁。
而不是:
“专门解决文件乱码的一个工具。”
4.6 FileReader 本身和 InputStreamReader 的关系
实际上:
FileReader
本身就是:
InputStreamReader
的子类。
关系:
Reader
↑
InputStreamReader
↑
FileReader
而:
FileWriter
的关系是:
Writer
↑
OutputStreamWriter
↑
FileWriter
所以你现在应该看到一个非常漂亮的体系:
InputStreamReader
解决通用 byte → char
FileReader
专门解决文件 byte → char
OutputStreamWriter
解决通用 char → byte
FileWriter
专门解决文件 char → byte
这就是继承与 IO 抽象体系真正结合起来的地方。
五、实践应用
5.1 GBK 文件转 UTF-8 文件
这是转换流非常经典的综合案例。
需求:
source-gbk.txt
↓
GBK 解码
↓
Java 字符
↓
UTF-8 编码
↓
target-utf8.txt
完整数据流:
source-gbk.txt
↓
FileInputStream
↓
InputStreamReader(GBK)
↓
BufferedReader
↓
String
↓
BufferedWriter
↓
OutputStreamWriter(UTF-8)
↓
FileOutputStream
↓
target-utf8.txt
代码:
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class CharsetConvertDemo {
public static void main(String[] args) throws IOException {
Charset gbk = Charset.forName("GBK");
try (
BufferedReader br =
new BufferedReader(
new InputStreamReader(
new FileInputStream("source-gbk.txt"),
gbk
)
);
BufferedWriter bw =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("target-utf8.txt"),
StandardCharsets.UTF_8
)
)
) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
}
}
}
这个程序真正做的是:
GBK byte
↓
GBK 解码
↓
Java 字符
↓
UTF-8 编码
↓
UTF-8 byte
这就是:
字符集转码。
5.2 网络文本通信
未来学习 Socket 时:
socket.getInputStream()
得到的是:
InputStream
如果协议传输文本,就可以:
BufferedReader br =
new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
StandardCharsets.UTF_8
)
);
发送文本:
BufferedWriter bw =
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream(),
StandardCharsets.UTF_8
)
);
因此今天学习转换流,其实已经在为:
Java 网络编程
铺路。
六、常见问题
6.1 InputStreamReader 是输入流还是输出流?
输入流。
继承关系:
Reader
↑
InputStreamReader
虽然名字里出现:
InputStream
但那表示:
它包装 InputStream。
最终得到的是:
Reader
所以它属于:
字符输入流
6.2 OutputStreamWriter 是字节流还是字符流?
字符输出流。
因为:
Writer
↑
OutputStreamWriter
它接收:
char/String
然后内部编码成:
byte
交给 OutputStream。
6.3 为什么 FileInputStream 不能直接指定 UTF-8?
因为:
FileInputStream
只负责:
byte
在字节层面根本不存在:
这是中文
这是英文
这是 UTF-8 字符
这些概念。
只有开始:
byte → char
解码时,字符集才真正参与。
所以:
FileInputStream
不负责解码。
6.4 UTF-8 文件使用 GBK 读取为什么乱码?
因为发生:
UTF-8 byte
↓
按照 GBK 规则解释
↓
错误字符
错误不是:
Java 不认识中文
而是:
解码规则和编码规则不一致。
6.5 InputStreamReader 和 FileReader 到底怎么选?
如果数据源就是文件,并且使用现代 JDK:
new FileReader(
file,
StandardCharsets.UTF_8
)
更加直接。
如果底层已经是:
InputStream
例如:
Socket InputStream
HTTP 输入流
进程输入流
其他字节流
那么使用:
InputStreamReader
更加通用。
因此不要机械背:
指定编码 = InputStreamReader
应该理解为:
byte → char = InputStreamReader
文件 → char = FileReader 也可以
6.6 Charset 和 String 字符集名称怎么选?
两种形式都存在:
new InputStreamReader(in, "UTF-8");
以及:
new InputStreamReader(
in,
StandardCharsets.UTF_8
);
现代 Java 更推荐:
Charset
例如:
StandardCharsets.UTF_8
因为:
- 不容易拼写错误;
- 语义更加清晰;
- 避免依赖字符集名称字符串;
- API 类型更加明确。
6.7 转换流能解决所有乱码吗?
不能。
它只能解决:
你已经知道真实字符集,并且使用正确字符集进行解码的问题。
如果:
文件本身已经被错误编码
或者:
字节数据已经损坏
单纯改变:
InputStreamReader
不能神奇恢复数据。
所以乱码问题必须沿着:
原始字符
↓
编码
↓
字节
↓
存储/传输
↓
解码
↓
目标字符
整个链路排查。
七、练习与验收
7.1 知识问答
- [ ] 什么是转换流?
- [ ] Java 中两个核心转换流分别是什么?
- [ ]
InputStreamReader的作用是什么? - [ ]
OutputStreamWriter的作用是什么? - [ ]
InputStreamReader属于字节流还是字符流? - [ ]
OutputStreamWriter属于字节流还是字符流? - [ ] 什么是编码?
- [ ] 什么是解码?
- [ ] 为什么编码与解码字符集不一致可能产生乱码?
- [ ] 为什么转换流被称为字节流与字符流之间的桥梁?
- [ ] JDK 21 中
FileReader能否直接指定Charset? - [ ] 既然
FileReader可以指定 Charset,为什么还需要InputStreamReader? - [ ]
StandardCharsets.UTF_8与"UTF-8"两种写法有什么区别? - [ ] GBK 文件转换成 UTF-8 文件本质经历了哪些步骤?
7.2 代码阅读
不运行下面程序:
BufferedReader br =
new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"),
StandardCharsets.UTF_8
)
);
回答:
- [ ]
FileInputStream负责什么? - [ ]
InputStreamReader负责什么? - [ ]
BufferedReader负责什么? - [ ] 数据从文件进入 Java 程序一共经历了哪些转换?
阅读:
BufferedWriter bw =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("data.txt"),
StandardCharsets.UTF_8
)
);
回答:
- [ ] 数据最终以字符还是字节形式保存在磁盘?
- [ ] UTF-8 编码在哪一层发生?
- [ ]
BufferedWriter的主要职责是什么? - [ ]
FileOutputStream的主要职责是什么?
7.3 手写代码
关闭 AI 自动补全,独立完成:
- [ ] 使用
InputStreamReader以 UTF-8 读取文本文件。 - [ ] 使用
InputStreamReader以 GBK 读取文本文件。 - [ ] 使用
OutputStreamWriter以 UTF-8 写出中文文本。 - [ ] 使用
OutputStreamWriter以 GBK 写出中文文本。 - [ ] 使用
BufferedReader + InputStreamReader按行读取 GBK 文件。 - [ ] 使用
BufferedWriter + OutputStreamWriter写出 UTF-8 文件。 - [ ] 使用 JDK 21 的
FileReader + Charset重写其中一个案例。
7.4 Debug
假设:
data.txt
真实编码为 GBK。
下面代码:
BufferedReader br =
new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"),
StandardCharsets.UTF_8
)
);
要求:
- [ ] 判断中文是否可能乱码。
- [ ] 找出根本原因。
- [ ] 修复代码。
- [ ] 画出错误情况下“编码 → 解码”的数据链。
分析:
InputStreamReader reader =
new InputStreamReader(
new FileInputStream("a.jpg"),
StandardCharsets.UTF_8
);
要求:
- [ ] 从语法层面是否可以建立该对象?
- [ ] 从业务语义上是否合理?
- [ ] 为什么图片等二进制文件不应该使用字符流处理?
7.5 综合训练
完成:
GBK 文本文件 → UTF-8 文本文件转换器
要求:
输入:
source-gbk.txt
输出:
target-utf8.txt
技术要求:
FileInputStream
InputStreamReader
BufferedReader
BufferedWriter
OutputStreamWriter
FileOutputStream
流程必须能够解释为:
GBK 字节
↓
GBK 解码
↓
Java 字符
↓
UTF-8 编码
↓
UTF-8 字节
完成代码后,再思考:
如果输入数据不是文件,而是
Socket.getInputStream(),整个程序需要修改哪一层?
7.6 本章验收
不查看资料,画出:
InputStreamReader
InputStream ───────────────────→ Reader
byte 解码 char
OutputStreamWriter
Writer ────────────────────────→ OutputStream
char 编码 byte
并能够口述:
编码是什么
解码是什么
为什么会乱码
InputStreamReader 做什么
OutputStreamWriter 做什么
BufferedReader 又做什么
为什么各种流可以嵌套组合
最后独立完成:
读取一个 GBK 文件,并将其转换成 UTF-8 文件。
如果这段代码不仅能写出来,而且能从:
字节 → 解码 → 字符 → 编码 → 字节
解释整个过程,那么转换流就已经真正掌握。