第 31 章:IO 流基础
学习目标
- 掌握 File / Path 文件操作
- 理解字节流与字符流的区别
- 掌握缓冲流与 NIO 便捷 API
一、File 与 Path
java
// 老 API:java.io.File
File file = new File("data/user.txt");
file.exists(); // 是否存在
file.isDirectory(); // 是否目录
file.length(); // 字节数
file.getAbsolutePath(); // 绝对路径
file.mkdirs(); // 创建多级目录
file.delete(); // 删除
// ✅ 新 API:java.nio.file(JDK 7+,推荐)
Path path = Path.of("data/user.txt");
Files.exists(path);
Files.size(path);
Files.createDirectories(path.getParent());
Files.deleteIfExists(path);为什么用 NIO?
File的 API 出错时只返回false,不告诉你原因;Files抛异常带明确信息。
二、IO 流全景
| 维度 | 字节流 | 字符流 |
|---|---|---|
| 单位 | byte(8 位) | char(16 位) |
| 适用 | 图片、视频、任意二进制 | 文本 |
| 基类 | InputStream / OutputStream | Reader / Writer |
| 编码 | 不处理 | 自动按字符集解码 |
判断标准:能用记事本打开看懂的 → 字符流;否则 → 字节流。
三、读写文本文件
java
// ✅ 最简单(JDK 11+):小文件一行搞定
String content = Files.readString(Path.of("a.txt"));
Files.writeString(Path.of("b.txt"), content);
// ✅ 按行读(大文件用 Stream,不会一次性加载到内存)
try (Stream<String> lines = Files.lines(Path.of("big.log"))) {
lines.filter(l -> l.contains("ERROR"))
.forEach(System.out::println);
}
// ✅ 传统缓冲流写法(要控制编码时)
try (BufferedReader br = Files.newBufferedReader(
Path.of("a.txt"), StandardCharsets.UTF_8)) { // ① 显式指定编码
String line;
while ((line = br.readLine()) != null) { // ② readLine 返回 null 表示读完
System.out.println(line);
}
}⚠️ 编码坑:不指定编码时用系统默认(Windows 中文是 GBK),跨平台会乱码。永远显式写 UTF-8。
四、复制文件(字节流)
java
// ✅ 一行搞定
Files.copy(Path.of("src.png"), Path.of("dst.png"),
StandardCopyOption.REPLACE_EXISTING);
// 手动实现(理解原理)
try (InputStream in = Files.newInputStream(Path.of("src.png"));
OutputStream out = Files.newOutputStream(Path.of("dst.png"))) {
byte[] buffer = new byte[8192]; // ① 8KB 缓冲区,减少系统调用次数
int len;
while ((len = in.read(buffer)) != -1) { // ② read 返回实际读取字节数,-1 表示结束
out.write(buffer, 0, len); // ③ 只写实际读到的长度,不能写整个 buffer
}
}为什么用 8192? 大多数文件系统块大小是 4KB,8KB 是吞吐与内存的经验平衡点,JDK 内部默认也是这个值。
五、缓冲流为什么快
性能差距:读 1MB 文件,无缓冲逐字节读比缓冲读慢 上百倍。
六、序列化
java
public class User implements Serializable { // ① 必须实现 Serializable
private static final long serialVersionUID = 1L; // ② 版本号,改类结构时兼容判断依据
private String name;
private transient String password; // ③ transient 字段不参与序列化
}
// 写对象
try (ObjectOutputStream oos = new ObjectOutputStream(
Files.newOutputStream(Path.of("user.dat")))) {
oos.writeObject(user);
}
// 读对象
try (ObjectInputStream ois = new ObjectInputStream(
Files.newInputStream(Path.of("user.dat")))) {
User u = (User) ois.readObject();
}⚠️ 生产建议:Java 原生序列化有安全漏洞(反序列化 RCE)且跨语言不通,实际项目用 JSON(Jackson) 或 Protobuf。
七、常见 IO 场景速查
java
// 读 classpath 资源(读配置文件常用)
try (InputStream in = getClass().getResourceAsStream("/config.yml")) {
String s = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
// 遍历目录
try (Stream<Path> paths = Files.walk(Path.of("src"))) {
paths.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// 追加写
Files.writeString(Path.of("log.txt"), "new line\n",
StandardOpenOption.CREATE, StandardOpenOption.APPEND);八、本章小结
| 要点 | 关键 |
|---|---|
| Path / Files | 优先于老的 File |
| 字节流 | 二进制,InputStream |
| 字符流 | 文本,Reader,必须指定 UTF-8 |
| 缓冲流 | 减少系统调用,快上百倍 |
| try-with-resources | 流一律这样写 |
| 序列化 | 生产用 JSON,别用原生 |
动手练习
练习 1:基础题
统计一个 .log 文件中包含 ERROR 的行数。
练习 2:进阶题
写一个方法递归统计目录下所有 .java 文件的总行数,输出 Top 10 最长文件。
下一章:第 32 章:IO 流进阶 →