Skip to content

Chapter 16 · IO 流与文件操作

把数据从一个地方搬到另一个地方 — 文件、网络、内存、屏幕。


🎯 本章目标

  • 理解 流(Stream) 的概念
  • 区分 字节流 vs 字符流输入流 vs 输出流
  • 掌握 BufferedReader/WriterFileInputStream/OutputStream
  • 学会 NIO.2 现代文件 API(Files / Path
  • 用对 try-with-resources 自动关流

1. 什么是流?

数据从源头到目的地的"管道"

生活类比:

  • 水龙头到水桶 = 输入流(读)
  • 水桶到水龙头 = 输出流(写)

数据可能是 字节(图片、视频、压缩包),也可能是 字符(文本)。

IO 流分类


2. 流的四大分类

维度分类
方向输入 Input(读)/ 输出 Output(写)
单位字节流 Byte(8 位)/ 字符流 Character(16 位)
功能节点流(直接连数据源)/ 处理流(包装其他流,加功能)
类型顶层抽象类典型实现
字节输入流InputStreamFileInputStream, BufferedInputStream
字节输出流OutputStreamFileOutputStream, BufferedOutputStream
字符输入流ReaderFileReader, BufferedReader
字符输出流WriterFileWriter, BufferedWriter, PrintWriter

💡 何时用哪种

  • 文本(.txt, .csv, .json, .xml) → 字符流(处理编码)
  • 二进制(.jpg, .mp4, .zip) → 字节流

3. 字节流读写

3.1 复制一张图片

java
try (FileInputStream in = new FileInputStream("source.jpg");
     FileOutputStream out = new FileOutputStream("copy.jpg")) {
    byte[] buf = new byte[4096];
    int n;
    while ((n = in.read(buf)) != -1) {
        out.write(buf, 0, n);
    }
}

3.2 包装成缓冲流(性能 100x!)

java
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("source.jpg"));
     BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("copy.jpg"))) {
    int b;
    while ((b = in.read()) != -1) {
        out.write(b);
    }
}

💡 为什么缓冲流快?把多次系统调用合并成一次(默认 8KB 缓冲)。


4. 字符流读写

4.1 逐行读取文本文件

java
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

4.2 写文本

java
try (BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"))) {
    bw.write("Hello\n");
    bw.write("World\n");
}

// 或者用 PrintWriter(自带 println)
try (PrintWriter pw = new PrintWriter("c.txt")) {
    pw.println("Hello");
    pw.printf("年龄 %d%n", 25);
}

4.3 处理编码(重要!)

java
// ⚠️ FileReader 用系统默认编码,跨平台坑
new FileReader("a.txt");  

// ✅ 显式指定编码
new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8);

// JDK 11+ 简化
Files.newBufferedReader(Paths.get("a.txt"), StandardCharsets.UTF_8);

5. NIO.2:现代化文件 API(JDK 7+)

File 类很烂(命名混乱、错误信息差),JDK 7 引入 NIO.2:Path + Files

NIO.2 API

5.1 Path 路径

java
Path p1 = Paths.get("data.txt");
Path p2 = Paths.get("/tmp", "logs", "app.log");
Path abs = p1.toAbsolutePath();
String fileName = p1.getFileName().toString();

5.2 Files 工具类(一行干完所有事)

java
// 读
String text = Files.readString(Paths.get("a.txt"));
List<String> lines = Files.readAllLines(Paths.get("a.txt"));
byte[] bytes = Files.readAllBytes(Paths.get("img.jpg"));

// 写
Files.writeString(Paths.get("b.txt"), "Hello");
Files.write(Paths.get("c.txt"), List.of("a", "b", "c"));

// 文件操作
Files.exists(p);
Files.size(p);
Files.delete(p);
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dst);
Files.createDirectory(Paths.get("logs"));
Files.createDirectories(Paths.get("a/b/c"));

// 流式读(大文件友好)
try (Stream<String> stream = Files.lines(Paths.get("big.log"))) {
    stream.filter(l -> l.contains("ERROR"))
          .forEach(System.out::println);
}

5.3 遍历目录

java
try (Stream<Path> walk = Files.walk(Paths.get("."))) {
    walk.filter(Files::isRegularFile)
        .filter(p -> p.toString().endsWith(".java"))
        .forEach(System.out::println);
}

6. 序列化与反序列化

把对象转成字节流,可以保存到磁盘 / 网络传输。

java
class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    int age;
    transient String password;     // transient 不参与序列化
}

// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("p.dat"))) {
    out.writeObject(new Person("张三", 25));
}

// 反序列化
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("p.dat"))) {
    Person p = (Person) in.readObject();
}

⚠️ 现代项目少用 Java 原生序列化

  • 性能差、体积大
  • 安全漏洞频出(CVE 一抓一大把)
  • 推荐 JSON(Jackson、Gson)Protobuf

7. 实战练习

文件内容
FileCopyDemo.java复制文件(字节流 + 缓冲)
TextReadDemo.java文本读写 + 编码
Nio2Demo.javaNIO.2 现代 API
DirectoryWalker.java遍历目录
LogAnalyzer.java实战:日志分析器

💡 运行前请创建 data/ 目录 并放入测试文件,或代码会自动生成 demo 数据。


8. 浏览器演示

打开 demo.html

  • IO 流分类树
  • 性能对比(带缓冲 vs 不带)
  • 文本编码可视化

9. 面试可能会问什么?

Q1: 字节流和字符流区别?

维度字节流字符流
单位byte (8 位)char (16 位)
适用二进制(图片、音视频)文本
编码不处理自动按 charset 解码
顶层InputStream / OutputStreamReader / Writer

Q2: 为什么要用缓冲流?

减少系统调用次数。FileInputStream.read() 每读一字节都要陷入内核态,缓冲流一次读 8KB 到内存,大幅提升性能(10~100 倍)。

Q3: try-with-resources 的优势?

  1. 自动关流,逆序关闭
  2. 异常时也能关
  3. 减少嵌套 try-finally
  4. 资源对象必须实现 AutoCloseable

Q4: NIO 和 IO 区别?

IO(BIO)NIO
流向单向双向(Channel)
数据流式Buffer
阻塞阻塞非阻塞(可选)
多路复用不支持Selector
适用简单、连接少高并发、连接多

Q5: serialVersionUID 是什么?

类序列化的版本号,反序列化时如果对不上 → InvalidClassException总是显式声明,否则编译器会基于类结构生成,类一改就反序列化失败。

Q6: transient 关键字?

修饰字段,告诉序列化机制 跳过它。常用于密码、敏感数据、临时字段。


🎁 本章小结

✅ 流 = 数据搬运管道
✅ 字节流处理二进制;字符流处理文本(注意编码)
✅ 缓冲流大幅提升性能
✅ NIO.2 (Files/Path) 是现代首选
✅ try-with-resources 自动关流
✅ Java 原生序列化已过时,用 JSON

🔗 导航