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

10.1 的 File 只是路标,搬数据要靠流。字节流 = 万能搬运工:硬盘和内存之间一根管道,一次搬一个字节——图片、视频、音乐、文本,只要是文件它都能搬。本节三大件:写(FileOutputStream)、读(FileInputStream)、复制(边读边写)。

1. 引入:流是什么

流(Stream)就是数据搬运的管道:数据像水一样从一头流向另一头。方向以内存为家:

为什么叫"字节"流?因为硬盘上的一切本来就是字节——图片是字节、视频是字节、文本也是字节。字节流不懂内容只管搬,所以万能;想要"看懂内容",那是字符流的活儿(后面的钩子)。

2. 写数据:FileOutputStream

FileOutputStream fos = new FileOutputStream("test\\b.txt");  // ① 接管道

fos.write(97);                        // ② 写一个字节 → a
fos.write("波仔666".getBytes());       //    字符串要先 getBytes() 拆成字节数组(6.2 的账)
fos.write("\r\n".getBytes());          //    换行:Windows 记事本认 \r\n
fos.write("第二行".getBytes());          // 追加写

fos.close();                          // ③ 关管道:释放资源 + 数据落袋

三条保命规矩(构造那一刻就决定了):

  1. 文件不存在 → 自动创建(注意:这和 new File 不一样!流构造是干活的,真建文件)。
  2. 文件存在 → 默认清空重来!想接着写,构造第二参数传 true: java new FileOutputStream("test\\b.txt", true); // 追加模式
  3. 必须 close()——不关,数据可能滞留在缓冲区没真正落盘,资源也一直占着。
📝 异常往哪甩?

FileOutputStream 构造和读写都会抛 IOException(受检,5.2 的账)。标准姿势是 try-catch-finally,close() 放 finally 里保证必执行(5.3 的账)——JDK7 之后还有更省事的 try-with-resources:

try (FileOutputStream fos = new FileOutputStream("b.txt")) {
    fos.write(97);
} catch (IOException e) {
    e.printStackTrace();
}   // 括号里声明的流,出括号自动 close,不用手关

3. 读数据:FileInputStream

FileInputStream fis = new FileInputStream("test\\b.txt");

int b;
while ((b = fis.read()) != -1) {      // 一次读一个字节
    System.out.print((char) b);
}
fis.close();

两个要点:

  1. read() 返回 int,不是 byte——因为"读完了"得有个信号,约定的哨兵值是 -1。可 -1 写成 8 个二进制 1,跟正常字节数据撞车;换成 4 字节的 int 装,正常数据永远是 0~255,-1 独一无二。哨兵机制,读循环的标准套路。
  2. 一次一个字节太慢——用"小推车"批量搬: java byte[] cart = new byte[1024]; int len; while ((len = fis.read(cart)) != -1) { // 返回本次实际搬了几个 System.out.print(new String(cart, 0, len)); // 只翻译前 len 个 }

4. 收网旧账

5. 动手:Demo06 复制一张图片(字节流的成人礼)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Demo06 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("photo.jpg");      // 源
        FileOutputStream fos = new FileOutputStream("photo_copy.jpg"); // 副本

        byte[] cart = new byte[1024];
        int len;
        long count = 0;
        while ((len = fis.read(cart)) != -1) {   // 搬一车
            fos.write(cart, 0, len);              // 卸一车
            count += len;
        }
        System.out.println("复制完成,共 " + count + " 字节");

        fos.close();
        fis.close();   // 后开的先关
    }
}

跑完打开副本——和原图一模一样。文本、图片、视频,复制代码一模一样:这就是"字节流万能"的含义。

三个 30 秒实验:

  1. 连跑两次 Demo06——副本没变?因为"复制"每次都是新建 + 全量覆盖。再试试用 new FileOutputStream("b.txt") 往已有文件写一句——原内容被清空了(保命规矩 2 的现场)。
  2. 把 fos.write(cart, 0, len) 改成 fos.write(cart) 再复制一个文本——末尾多了一串空字符:小推车没卸干净,上次剩的旧货也被写出去了(所以必须带 0, len)。
  3. 用 read() 单字节版逐个打印 b.txt 内容——中文变乱码或缺一半:中文占多个字节,单字节打印把"半个字"当独立数据翻了。字节流管搬不管翻译——翻译是字符流的活(下节钩子)。

6. 常见误区

⚠️ 五个坑
  1. ❌ 以为 new FileOutputStream 没副作用 —— 文件不在会自动建;文件在会清空!要追加必须第二参数传 true。
  2. ❌ 忘 close() —— 数据滞留缓冲区没落盘(文件空的/缺尾巴),资源也一直占着。close 是收工仪式,不是可选项。
  3. ❌ Windows 下用 \n 换行 —— 记事本不认,要 \r\n;写代码里就是 "\r\n".getBytes()。
  4. ❌ 把 read() 的返回值当 byte 收 —— 哨兵 -1 要求用 int 接;byte b 永远判断不到"读完"。
  5. ❌ 用字节流读中文文本并直接打印 —— 中文多字节被拆散,打印乱码。字节流复制文本没事(原样搬运),但"读出来给人看"要等字符流。

7. 可折叠自测

❓ Q1:`new FileOutputStream("b.txt")` 和 `new File("b.txt")` 对磁盘的影响有什么不同?

前者真干活:文件不在自动创建,在则清空;后者只是内存里造路标,磁盘纹丝不动。追加写要给构造传第二参数 true。

❓ Q2:为什么 `read()` 返回 int 而不是 byte?

需要"读完"的哨兵值 -1。byte 是 8 位,-1 的 8 个 1 和正常数据撞车;int 是 32 位,正常数据 0~255,-1 独一份。

❓ Q3:`read(byte[])` 的返回值是什么?为什么 `new String(cart, 0, len)` 不能写成 `new String(cart)`?

返回本次实际读到的字节个数。cart 是复用的小推车,末尾可能残留上一次的旧数据,直接翻译全车会把旧货也翻出来——只翻前 len 个。

❓ Q4:try-with-resources 相比 finally 关流好在哪?

把流声明在 try 后的括号里,出括号自动 close——即使中途抛异常也保证关,代码少一层嵌套,不用手写 finally。

❓ Q5(毕业题):不看笔记,写出"文件搬运工":把任意文件复制一份,复制完打印总字节数,并用 try-with-resources 管理两个流。
try (FileInputStream fis = new FileInputStream("src.jpg");
     FileOutputStream fos = new FileOutputStream("dst.jpg")) {
    byte[] cart = new byte[1024];
    int len; long count = 0;
    while ((len = fis.read(cart)) != -1) {
        fos.write(cart, 0, len);
        count += len;
    }
    System.out.println("共复制 " + count + " 字节");
} catch (IOException e) {
    e.printStackTrace();
}

写出来,你就能用 Java 干"Ctrl+C / Ctrl+V"底层干的活了。

8. 下一步学什么

相关笔记