转换流就是能自己带词典的翻译官:OutputStreamWriter / InputStreamReader 站在字节流和字符流中间,读写时按你指定的编码翻译——10.3 的毕业题「GBK → UTF-8 转码器」,这节亲手写出来。
为什么单开一节:便捷款的词典是锁死的
10.3 说过:字符流 = 字节流 + 翻译官,翻译要查编码表。但 FileWriter / FileReader 这两个便捷款有个死穴——它们用的词典跟系统默认走,你换不了。
问题就出在这:
| 场景 | 系统默认 | 文件实际编码 | 结果 |
|---|---|---|---|
| Windows 老记事本写文件 | GBK(中文系统) | GBK | 没事 |
| IDEA / VS Code 写代码 | UTF-8 | UTF-8 | 没事 |
| 跨设备互传(手机→电脑、同事发来的 CSV) | 不一样 | 不一样 | 乱码 |
10.3 的乱码铁律说「文件没坏,是查错了表」。可 FileWriter 查哪张表你说了不算——要指定编码,就得亲自把翻译官 hire 进管道,这就是转换流。
家族定位:它是 Writer/Reader 的儿子,字节流的雇主
再看一遍 10.3 的家族树,这次盯住中间层:
Writer(抽象父类)
└── OutputStreamWriter ← 转换流:字符→字节,可指定编码
└── FileWriter ← 便捷款:编码固定跟系统
Reader(抽象父类)
└── InputStreamReader ← 转换流:字节→字符,可指定编码
└── FileReader ← 便捷款:编码固定跟系统
看名字就懂继承关系:File**Writer** 是 Output**StreamWriter** 的子类——便捷款其实是转换流的「傻瓜版」。
构造方法(重点就两个参数):
// 写出侧:包一层字节流,第二参指定编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("a.txt"), "GBK");
// 读入侧:同理
InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"), "GBK");
- 第一参:一个字节流(翻译官自己不会搬东西,雇个搬运工在下面)
- 第二参:编码名,字符串
"GBK"/"UTF-8";不写就走系统默认(那就退化成 FileWriter 了) - 编码名写错(如
"gbk123")→ 运行时抛UnsupportedEncodingException,属于 IllegalArgumentException 家族——运行时异常,编译器不拦,得自己长眼
记忆锚点:流的名字 = 它「对外说人话」的方向。OutputStreamWriter 是个 Writer(字符流),但它的 Out 指向字节海洋——你往里 write 字符,它翻成字节吐给下面的字节流。InputStreamReader 同理:从字节海里 read 字节,翻成字符交给你。
写出侧实操:往 GBK 文件里写中文
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("day11\\gbk.txt"), "GBK")) {
osw.write("波仔的 Java 笔记");
osw.write("\r\n"); // 换行自己写(10.2 讲过:\r\n 才是 Windows 标准)
osw.write("第二行");
} // try-with-resources 自动关流,关流前自动 flush
用记事本/Notepad 打开看右下角编码:显示 ANSI(中文 Windows 下就是 GBK)——文件真按 GBK 存的。如果你在 IDEA(UTF-8 环境)直接打开它,就是乱码——不是坏了,词典没对上。
读入侧实操:把 GBK 文件读回来
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("day11\\gbk.txt"), "GBK")) {
int ch;
while ((ch = isr.read()) != -1) { // -1 哨兵照旧(10.2 的账)
System.out.print((char) ch);
}
}
read()返回int——但这次返回的已经是翻译好的字符,不是 10.2 字节流那个"生字节"了- 一个字符一个字符读效率低,还是老套路换小推车:
char[] cart = new char[1024]; read(cart)返回本次个数
读的时候编码必须和文件实际编码一致。文件是 GBK,你用 UTF-8 读 → 乱码;文件是 UTF-8,你用 GBK 读 → 乱码。转换流只负责「你指定哪张表我就查哪张」,它不会自动认出文件是什么编码(Java 基础阶段没有「探测编码」的 API,见到乱码先想表对不对)。
压轴实战:GBK → UTF-8 转码器(10.3 毕业题落成)
思路一句话:按 GBK 翻进来,按 UTF-8 翻出去——数据在内存里以字符形式中转,两头词典不同,内容不丢。
public class Demo08 {
public static void main(String[] args) throws IOException {
// 读:文件是 GBK,就用 GBK 词典翻进来
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("day11\\gbk.txt"), "GBK");
// 写:目标要 UTF-8,就用 UTF-8 词典翻出去
OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("day11\\utf8.txt"), "UTF-8")) {
char[] cart = new char[1024];
int len;
while ((len = isr.read(cart)) != -1) {
osw.write(cart, 0, len); // 只写本次读到的个数(10.2 残渣坑)
}
}
System.out.println("转码完成");
}
}
char[]缓冲是中转站:字节流复制用byte[],字符流复制用char[]——各自搬各的- 这就是「以内存为家」的活例子:字节进内存 → 翻成字符 → 再翻成另一种编码的字节出内存
- 套娃四层:
File → FileInputStream → InputStreamReader → read,读侧对称。看着长,其实是每层只干一件事的标准姿势
用 FileWriter 写个文件,去 Notepad 看编码——跟着系统走(中文 Windows 是 ANSI/GBK)。再试试 new FileWriter("x.txt", Charset.forName("UTF-8"))——编译直接报错,FileWriter 根本没有这个构造方法。指定编码只有转换流能做。
new OutputStreamWriter(fos, "gbk123") → 运行时 UnsupportedEncodingException。注意它不是编译错误——编译器不认识编码名,只能运行时炸(受 5.2 分类框架管:这货是运行时异常)。
System.out.println(System.getProperty("file.encoding")); ——看看你家 JVM 默认查哪张表。IDEA 里跑一般是 UTF-8,这解释了为什么 IDEA 打开 GBK 文件会乱码。
常见误区
- 「转换流是新的一种流」——不是,它就是字符流家族的成员(Writer/Reader 子类),只是兼职当翻译官。本质:字符流 = 字节流 + 翻译官,转换流把翻译官单独拎出来给你发工资(指定词典)。
- 「FileWriter 也能指定编码,加个参数就行」——不行,构造方法里根本没有编码参数。要指定编码必须
OutputStreamWriter。 - 「转换流能自动识别文件编码」——不能。它只会查你指定的表。文件实际是 GBK 你写 UTF-8,照样乱码。编码探测在基础阶段没有 API,靠人看。
- 「编码名是编译期检查的」——不是,写错运行时才炸(
UnsupportedEncodingException),而且测试时不容易发现,因为多数人只测 UTF-8。 - 「读 GBK 文件乱码是文件坏了」——10.3 铁律再念一遍:文件没坏,词典没对上。修复方式不是重下文件,是换对编码读。
自测一下
字符流(Writer 的子类)。名字里的 OutputStream 指它下面雇的字节流——它自己以字符为接口(write(String)),内部把字符翻成字节交给底下的 FileOutputStream。
一种:转换流。new OutputStreamWriter(new FileOutputStream("a.txt"), "GBK")。FileWriter 没有编码参数,写不了 GBK(中文 Windows 默认恰好是 GBK 算巧合,不算指定)。
都返回 int,-1 都是哨兵。区别:字节流的 read 返回原始字节值(0~255);转换流的 read 返回翻译好的字符(char 的码值)。一个生一个熟。
不能。转换流读出来的已经是字符,往 byte[] 里装不了(类型对不上)。而且用 byte[] 就绕开了翻译环节,等于退回字节流——那就没法按不同编码翻进翻出了。
三步:①按 GBK 建读侧管道 InputStreamReader(new FileInputStream("data.csv"), "GBK");②按 UTF-8 建写侧管道 OutputStreamWriter(new FileOutputStream("data_utf8.csv"), "UTF-8");③char[] 循环搬运 while((len=isr.read(cart))!=-1) osw.write(cart,0,len);。要点:读侧词典=源文件编码,写侧词典=目标编码,搬运过程数据以字符中转。
下一步学什么
- ☑ 10.4 转换流 ✅ —— 点此复习:第十章 I-O/10.4 转换流
- ☑ 10.5 序列化和反序列化 ✅ —— 点此复习:第十章 I-O/10.5 序列化和反序列化
相关笔记
- 第十章 I-O/10.3 字符流:翻译官首次出场、家族树、flush vs close——本节的根基
- 第十章 I-O/10.2 字节流:-1 哨兵、小推车、
write(cart, 0, len)——搬运套路全盘继承 - 第十章 I-O/10.1 File类:路标与文件本体
- 10. 🔵 计算机基础/02 信息的数字化表示:编码表的底层常识(计算机基础系列)
- 第六章 Java API/6.2 字符串类:getBytes 按指定编码出字节——和转换流是同一件事的两种入口