主题
Chapter 16 · IO 流与文件操作
把数据从一个地方搬到另一个地方 — 文件、网络、内存、屏幕。
🎯 本章目标
- 理解 流(Stream) 的概念
- 区分 字节流 vs 字符流、输入流 vs 输出流
- 掌握
BufferedReader/Writer、FileInputStream/OutputStream - 学会 NIO.2 现代文件 API(
Files/Path) - 用对 try-with-resources 自动关流
1. 什么是流?
数据从源头到目的地的"管道"。
生活类比:
- 水龙头到水桶 = 输入流(读)
- 水桶到水龙头 = 输出流(写)
数据可能是 字节(图片、视频、压缩包),也可能是 字符(文本)。
2. 流的四大分类
| 维度 | 分类 |
|---|---|
| 方向 | 输入 Input(读)/ 输出 Output(写) |
| 单位 | 字节流 Byte(8 位)/ 字符流 Character(16 位) |
| 功能 | 节点流(直接连数据源)/ 处理流(包装其他流,加功能) |
| 类型 | 顶层抽象类 | 典型实现 |
|---|---|---|
| 字节输入流 | InputStream | FileInputStream, BufferedInputStream |
| 字节输出流 | OutputStream | FileOutputStream, BufferedOutputStream |
| 字符输入流 | Reader | FileReader, BufferedReader |
| 字符输出流 | Writer | FileWriter, 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。
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.java | NIO.2 现代 API |
DirectoryWalker.java | 遍历目录 |
LogAnalyzer.java | 实战:日志分析器 |
💡 运行前请创建
data/目录 并放入测试文件,或代码会自动生成 demo 数据。
8. 浏览器演示
打开 demo.html:
- IO 流分类树
- 性能对比(带缓冲 vs 不带)
- 文本编码可视化
9. 面试可能会问什么?
Q1: 字节流和字符流区别?
| 维度 | 字节流 | 字符流 |
|---|---|---|
| 单位 | byte (8 位) | char (16 位) |
| 适用 | 二进制(图片、音视频) | 文本 |
| 编码 | 不处理 | 自动按 charset 解码 |
| 顶层 | InputStream / OutputStream | Reader / Writer |
Q2: 为什么要用缓冲流?
减少系统调用次数。FileInputStream.read() 每读一字节都要陷入内核态,缓冲流一次读 8KB 到内存,大幅提升性能(10~100 倍)。
Q3: try-with-resources 的优势?
- 自动关流,逆序关闭
- 异常时也能关
- 减少嵌套 try-finally
- 资源对象必须实现
AutoCloseable
Q4: NIO 和 IO 区别?
| IO(BIO) | NIO | |
|---|---|---|
| 流向 | 单向 | 双向(Channel) |
| 数据 | 流式 | Buffer |
| 阻塞 | 阻塞 | 非阻塞(可选) |
| 多路复用 | 不支持 | Selector |
| 适用 | 简单、连接少 | 高并发、连接多 |
Q5: serialVersionUID 是什么?
类序列化的版本号,反序列化时如果对不上 → InvalidClassException。总是显式声明,否则编译器会基于类结构生成,类一改就反序列化失败。
Q6: transient 关键字?
修饰字段,告诉序列化机制 跳过它。常用于密码、敏感数据、临时字段。
🎁 本章小结
✅ 流 = 数据搬运管道
✅ 字节流处理二进制;字符流处理文本(注意编码)
✅ 缓冲流大幅提升性能
✅ NIO.2 (Files/Path) 是现代首选
✅ try-with-resources 自动关流
✅ Java 原生序列化已过时,用 JSON🔗 导航
- ⬅️ 上一章:Chapter 15 · 泛型
- ➡️ 下一章:Chapter 17 · 多线程与并发基础