☕ 第十章 I/O
📄 一句话概括

转换流就是能自己带词典的翻译官: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");
⚠️ 名字别背反

记忆锚点:流的名字 = 它「对外说人话」的方向。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);
    }
}
📝 死穴条款

读的时候编码必须和文件实际编码一致。文件是 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("转码完成");
    }
}
💡 30 秒实验一:验证便捷款真的换不了词典

用 FileWriter 写个文件,去 Notepad 看编码——跟着系统走(中文 Windows 是 ANSI/GBK)。再试试 new FileWriter("x.txt", Charset.forName("UTF-8"))——编译直接报错,FileWriter 根本没有这个构造方法。指定编码只有转换流能做。

💡 30 秒实验二:故意写错编码名

new OutputStreamWriter(fos, "gbk123") → 运行时 UnsupportedEncodingException。注意它不是编译错误——编译器不认识编码名,只能运行时炸(受 5.2 分类框架管:这货是运行时异常)。

💡 30 秒实验三:查问系统默认编码

System.out.println(System.getProperty("file.encoding")); ——看看你家 JVM 默认查哪张表。IDEA 里跑一般是 UTF-8,这解释了为什么 IDEA 打开 GBK 文件会乱码。

常见误区

  1. 「转换流是新的一种流」——不是,它就是字符流家族的成员(Writer/Reader 子类),只是兼职当翻译官。本质:字符流 = 字节流 + 翻译官,转换流把翻译官单独拎出来给你发工资(指定词典)。
  2. 「FileWriter 也能指定编码,加个参数就行」——不行,构造方法里根本没有编码参数。要指定编码必须 OutputStreamWriter。
  3. 「转换流能自动识别文件编码」——不能。它只会查你指定的表。文件实际是 GBK 你写 UTF-8,照样乱码。编码探测在基础阶段没有 API,靠人看。
  4. 「编码名是编译期检查的」——不是,写错运行时才炸(UnsupportedEncodingException),而且测试时不容易发现,因为多数人只测 UTF-8。
  5. 「读 GBK 文件乱码是文件坏了」——10.3 铁律再念一遍:文件没坏,词典没对上。修复方式不是重下文件,是换对编码读。

自测一下

❓ Q1:`OutputStreamWriter` 是字节流还是字符流?为什么?

字符流(Writer 的子类)。名字里的 OutputStream 指它下面雇的字节流——它自己以字符为接口(write(String)),内部把字符翻成字节交给底下的 FileOutputStream。

❓ Q2:要用 GBK 编码写文件,最少几种写法?分别是什么?

一种:转换流。new OutputStreamWriter(new FileOutputStream("a.txt"), "GBK")。FileWriter 没有编码参数,写不了 GBK(中文 Windows 默认恰好是 GBK 算巧合,不算指定)。

❓ Q3:`InputStreamReader.read()` 返回什么?和 10.2 字节流的 `read()` 有何区别?

都返回 int,-1 都是哨兵。区别:字节流的 read 返回原始字节值(0~255);转换流的 read 返回翻译好的字符(char 的码值)。一个生一个熟。

❓ Q4:转码器里 `char[]` 能换成 `byte[]` 吗?为什么?

不能。转换流读出来的已经是字符,往 byte[] 里装不了(类型对不上)。而且用 byte[] 就绕开了翻译环节,等于退回字节流——那就没法按不同编码翻进翻出了。

❓ Q5(毕业题):同事发来一个 GBK 编码的 `data.csv`,你的项目全是 UTF-8。写出把它转成 UTF-8 的完整思路和核心代码。

三步:①按 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);。要点:读侧词典=源文件编码,写侧词典=目标编码,搬运过程数据以字符中转。

下一步学什么

相关笔记