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

字符流 = 字节流 + 翻译官。字节流只管搬字节、看不懂内容;字符流读写时自动查编码表做"字节 ↔ 字符"转换——文本看得懂了,但只能碰文本。本节四件套:编码表常识、FileWriter/FileReader 写读、转换流指定编码、flush 与 close 的区别。

1. 引入:为什么需要翻译官

10.2 的实验三还记着:字节流逐字节读中文,打印出来一坨乱码。原因很简单——

字符流的解法:读的时候按编码表把字节拼成字符再给你,写的时候把字符按编码表拆成字节再落盘。翻译的活儿它包了,你只跟"字"打交道。

📝 乱码铁律(背下来)

乱码 ≠ 文件坏了。文件里的字节原封没动,坏的是"翻译"这一步——用什么编码写,就得用什么编码读。UTF-8 写、GBK 读,必乱;反过来也乱。所有乱码问题,先查两边编码是否一致。

2. 字符流家族:两大家 + 桥梁

写字用 Writer 家,读用 Reader 家(注意命名:都是 er 结尾,字节流是 Stream 结尾):

日常写小文本用便捷款;要指定编码(比如明确存成 UTF-8),就套一层转换流:

Writer w = new OutputStreamWriter(new FileOutputStream("b.txt"), "UTF-8");

3. 写文本:FileWriter

FileWriter fw = new FileWriter("test\\c.txt");   // 三保命规矩同字节流:自动建/清空/true 追加

fw.write("波仔学 IO");         // 直接写字符串!不用 getBytes 了
fw.write("\r\n");              // 换行照旧
fw.write(98);                  // 写单个字符(按码表:98 = b)

fw.close();                    // 关门(内含冲洗)

注意 write(int) 在字符流里的含义变了:字节流写的是"原始字节 97",字符流写的是"码表中 97 号字符"——翻译官上岗,同一句代码含义升级。

flush 与 close:冲一把 vs 关门

4. 读文本:FileReader

FileReader fr = new FileReader("test\\c.txt");

int ch;
while ((ch = fr.read()) != -1) {          // 一次读一个"字符",-1 哨兵照旧
    System.out.print((char) ch);           // 中文不乱码了!
}
fr.close();

小推车版(读得快,还能按段处理):

char[] cart = new char[1024];
int len;
while ((len = fr.read(cart)) != -1) {
    System.out.print(new String(cart, 0, len));
}

5. 收网旧账

6. 动手:Demo07 写中文、读中文、验乱码

import java.io.FileWriter;
import java.io.FileReader;
import java.io.IOException;

public class Demo07 {
    public static void main(String[] args) throws IOException {
        // 一、写中文(不用 getBytes,直接写字符串)
        FileWriter fw = new FileWriter("story.txt");
        fw.write("波仔在第十章学会了字符流\r\n");
        fw.write("翻译官上岗,乱码退散");
        fw.close();

        // 二、读中文(-1 哨兵 + 小推车)
        FileReader fr = new FileReader("story.txt");
        char[] cart = new char[1024];
        int len;
        while ((len = fr.read(cart)) != -1) {
            System.out.print(new String(cart, 0, len));
        }
        fr.close();

        // 三、指定编码写(转换流上岗)
        java.io.Writer w = new java.io.OutputStreamWriter(
                new java.io.FileOutputStream("utf8.txt"), "UTF-8");
        w.write("这份文件保证是 UTF-8 编码");
        w.close();
    }
}

三个 30 秒实验:

  1. 写完不 close 也不 flush,直接看 story.txt——空的!数据还压在缓冲区里。加上 flush 再看,有了而且流还能继续写。
  2. 用记事本"另存为"分别选 ANSI(GBK) 和 UTF-8 存两个文件,再分别用程序读——编码对上就正常,对不上就乱码。亲眼验证乱码铁律。
  3. 拿字符流去复制一张 jpg(read/write 走一遍)——副本打不开!字符流翻译时字节被重新分组,二进制文件当场损坏。复制活儿永远归字节流。

7. 常见误区

⚠️ 五个坑
  1. ❌ 用字符流复制图片/视频 —— 翻译过程会破坏原始字节,副本损坏。字符流只碰文本,搬运一律字节流。
  2. ❌ 写完不 close 不 flush 就去开文件 —— 字符流带内置缓冲,不冲不关文件就是空的。
  3. ❌ 给 FileWriter 传编码参数 —— 它没有这个构造!要指定编码请用转换流 OutputStreamWriter(new FileOutputStream(...), "UTF-8")。
  4. ❌ 文件乱码就以为写坏了 —— 文件没坏,读写编码不一致而已。统一两边编码,乱码自愈。
  5. ❌ 以为字符流更快所以无脑用 —— 字符流多一道翻译工序,二进制反而更慢还危险。选型口诀:看内容还是看字节?人看文本用字符流,机器搬东西用字节流。

8. 可折叠自测

❓ Q1:UTF-8 和 GBK 里一个汉字各占几个字节?乱码的根本原因是什么?

UTF-8 一个汉字 3 字节,GBK 2 字节。乱码根因:读写编码不一致,字节被按错误的表分组翻译——文件本身没坏。

❓ Q2:`flush()` 和 `close()` 的区别?各适合什么场景?

flush 只把缓冲区冲到硬盘,流还能继续用(适合长文本边写边冲);close 先冲再关,流不能再用(收工必调)。不冲不关文件可能是空的。

❓ Q3:`FileWriter` 和 `OutputStreamWriter` 怎么选?

用默认编码写小文本 → FileWriter 便捷款;要明确指定编码(如存成 UTF-8)→ 转换流套字节流:new OutputStreamWriter(new FileOutputStream(...), "编码")。

❓ Q4:字符流的 `write(97)` 和字节流的 `write(97)` 有什么本质区别?

字节流写的是原始字节 97;字符流查码表写的是"97 号字符"(小写 b)编码后的字节。同一句代码,翻译官上岗后语义升级。

❓ Q5(毕业题):不看笔记,写出"编码转换器":把一份 GBK 编码的 old.txt 读进来,按 UTF-8 编码存成 new.txt(提示:读用转换流指定 GBK,写用转换流指定 UTF-8)。
try (Reader r = new InputStreamReader(new FileInputStream("old.txt"), "GBK");
     Writer w = new OutputStreamWriter(new FileOutputStream("new.txt"), "UTF-8")) {
    char[] cart = new char[1024];
    int len;
    while ((len = r.read(cart)) != -1) {
        w.write(cart, 0, len);
    }
} catch (IOException e) {
    e.printStackTrace();
}

写出来,你就掌握了"乱码修复"的正确姿势——业内叫转码,本质就是换个表重新翻译。

9. 下一步学什么

相关笔记