IO 与 NIO
IO 与 NIO
本篇是《Java 从入门到精通》第 07 篇。IO 是程序与外部(文件、网络、控制台)交互的基础。本篇覆盖传统 IO 流体系、缓冲流、编码转换、NIO 三大组件、对象序列化,以及日常最常用的
Path/Files现代 API。
1. IO 流体系总览
Java IO 以"流"为核心,按维度分四类:
| 字节流(单位 byte) | 字符流(单位 char) | |
|---|---|---|
| 输入 | InputStream | Reader |
| 输出 | OutputStream | Writer |
- 字节流:处理任意二进制数据(图片、视频、PDF)。基类
InputStream/OutputStream。 - 字符流:处理文本,自动处理编码。基类
Reader/Writer。 - 节点流:直接连数据源(
FileInputStream、FileReader)。 - 处理流(装饰器):包装节点流增强功能(
BufferedInputStream、InputStreamReader)。
字节流 vs 字符流
读文本用字符流(自动编码);读二进制(图片、压缩包)用字节流。文本用字节流读可能乱码。
2. 字节流
2.1 FileInputStream / FileOutputStream
import java.io.*;
// 写
try (FileOutputStream fos = new FileOutputStream("data.bin")) {
fos.write(72); // 写一个字节
byte[] bytes = {101, 108, 108, 111};
fos.write(bytes); // 写字节数组 -> "Hello"
}
// 读
try (FileInputStream fis = new FileInputStream("data.bin")) {
int b;
while ((b = fis.read()) != -1) { // 逐字节读,-1 表示结束
System.out.print((char) b); // Hello
}
}
read() 逐字节读效率低,实际用缓冲区批量读:
try (FileInputStream fis = new FileInputStream("data.bin")) {
byte[] buf = new byte[1024];
int n;
while ((n = fis.read(buf)) != -1) { // 批量读入 buf,返回实际读取数
System.out.write(buf, 0, n);
}
}
read() 返回 int
read() 返回 int(0~255),用 byte 接收会与 -1(结束标志)冲突。务必用 int。
3. 字符流
3.1 FileReader / FileWriter
// 写文本
try (FileWriter fw = new FileWriter("note.txt")) {
fw.write("第一行\n");
fw.write("第二行\n");
}
// 读文本
try (FileReader fr = new FileReader("note.txt")) {
char[] buf = new char[1024];
int n;
while ((n = fr.read(buf)) != -1) {
System.out.print(new String(buf, 0, n));
}
}
FileReader/FileWriter 使用系统默认编码,跨平台可能乱码。需指定编码时用转换流(见 5)。
4. 缓冲流(性能必备)
逐字节/逐字符读写要频繁系统调用,性能差。缓冲流在内存加缓冲区,批量读写,性能提升数十倍。
// 字节缓冲流
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("big.zip"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.zip"))) {
byte[] buf = new byte[8192];
int n;
while ((n = bis.read(buf)) != -1) {
bos.write(buf, 0, n);
}
}
// 字符缓冲流(按行读写文本的最佳方式)
try (BufferedReader br = new BufferedReader(new FileReader("note.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"))) {
String line;
while ((line = br.readLine()) != null) { // 逐行读,返回 null 表示结束
bw.write(line.toUpperCase());
bw.newLine(); // 跨平台换行
}
}
缓冲流是装饰器模式
new BufferedInputStream(new FileInputStream(...)) 是经典装饰器--缓冲流包装节点流,增强功能而接口不变。所有 IO 流都遵循此模式。
写入后要 flush/close
缓冲流数据先存内存缓冲区,未满不会自动写入。close() 会自动 flush(),但若需中途确保落盘,手动 flush()。
5. 转换流与编码
InputStreamReader/OutputStreamWriter 是字节流与字符流的桥梁,可指定编码:
import java.nio.charset.StandardCharsets;
// 用 UTF-8 读
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("note.txt"), StandardCharsets.UTF_8))) {
System.out.println(br.readLine());
}
// 用 GBK 写
try (Writer w = new OutputStreamWriter(
new FileOutputStream("gbk.txt"), "GBK")) {
w.write("中文内容");
}
编码常识
- 文本文件本质是字节,编码决定如何解释字节为字符。
StandardCharsets.UTF_8是推荐的常量用法(避免拼写错误和依赖系统编码)。- 乱码 = 读时编码与写时不一致。统一用 UTF-8 可避免绝大多数问题。
6. 打印流
PrintStream/PrintWriter 提供方便的格式化输出,System.out 就是 PrintStream。
try (PrintWriter pw = new PrintWriter(new FileWriter("log.txt"))) {
pw.println("一行日志");
pw.printf("格式化:%s %d %.2f%n", "Tom", 18, 1.75);
}
PrintStream 的方法不抛 IOException(内部吞错,用 checkError() 检查),便于简单输出。
7. Path 与 Files(现代 API,推荐)
Java 7 引入 NIO.2 的 java.nio.file.Path 和 Files,比传统 File 更强大、更安全,日常优先用。
7.1 Path
import java.nio.file.*;
Path p = Path.of("dir", "sub", "file.txt"); // dir/sub/file.txt
p.getFileName(); // file.txt
p.getParent(); // dir/sub
p.toAbsolutePath(); // 绝对路径
Files.exists(p); // 是否存在
7.2 Files 常用操作
// 创建
Files.createDirectories(Path.of("a/b/c")); // 递归创建目录
Files.writeString(Path.of("a.txt"), "内容"); // 写文本(覆盖)
Files.write(Path.of("b.bin"), new byte[]{1, 2, 3}); // 写字节
// 读取(一行搞定,小文件)
String content = Files.readString(Path.of("a.txt")); // 读全部文本(UTF-8)
List<String> lines = Files.readAllLines(Path.of("a.txt")); // 读全部行
byte[] bytes = Files.readAllBytes(Path.of("b.bin")); // 读全部字节
// 大文件按行流式读(不一次性加载内存)
try (var stream = Files.lines(Path.of("big.log"))) {
stream.filter(l -> l.contains("ERROR"))
.forEach(System.out::println);
}
// 复制、移动、删除
Files.copy(Path.of("a.txt"), Path.of("a.bak"), StandardCopyOption.REPLACE_EXISTING);
Files.move(Path.of("a.txt"), Path.of("a.new"));
Files.delete(Path.of("a.new")); // 不存在会抛异常
Files.deleteIfExists(Path.of("a.new")); // 不存在不报错
// 遍历目录
try (var stream = Files.list(Path.of("."))) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// 递归遍历(含子目录)
try (var stream = Files.walk(Path.of("."))) {
stream.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
Files vs 传统 File
java.io.File 功能弱、API 设计混乱(如 delete 失败只返回 false 无原因)、不支持符号链接。新代码一律用 Path + Files。File 可通过 toPath() 转换。
大文件别 readAll
Files.readAllBytes/readAllLines 一次性加载全部内容,大文件会 OOM。大文件用 Files.lines() 流式读取或缓冲流分块。
8. NIO 概览
NIO(New IO,Java 1.4)三大核心:Buffer(缓冲区)、Channel(通道)、Selector(选择器)。相比传统 IO 的阻塞流,NIO 支持非阻塞、面向缓冲,适合高并发。
8.1 Buffer
缓冲区是数据容器,核心属性 position/limit/capacity。
import java.nio.ByteBuffer;
ByteBuffer buf = ByteBuffer.allocate(1024); // 分配堆内缓冲区
buf.put((byte) 1);
buf.putInt(100);
buf.flip(); // 切换为读模式(写->读必须 flip)
System.out.println(buf.getInt()); // 读出 1(前面的 byte)
buf.clear(); // 清空,准备再写
// 直接缓冲区(堆外内存,零拷贝,分配贵但 IO 快)
ByteBuffer direct = ByteBuffer.allocateDirect(1024);
flip() 是关键
Buffer 写后要读必须 flip()(把 limit 设为当前 position,position 归零)。忘 flip 是 NIO 最常见 bug。
8.2 Channel
通道是双向的,可读可写,配合 Buffer 使用:
import java.nio.channels.*;
import java.nio.file.*;
try (FileChannel fc = FileChannel.open(Path.of("a.txt"), StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
while (fc.read(buf) != -1) {
buf.flip();
// 处理 buf 中数据
buf.clear();
}
}
8.3 Selector
Selector 让单线程管理多个 Channel(多路复用),是 Netty、Tomcat NIO 的核心。原理:Channel 注册到 Selector,Selector 轮询就绪事件(读/写/连接),单线程处理多个连接。详细实现超出入门范围,记住它是 NIO 高并发的基石即可。
IO vs NIO 何时用
- 传统 IO(流):简单、阻塞、连接数少。
- NIO:非阻塞、多路复用、高并发连接(聊天服务器、网关)。
- 日常文件读写用
Files;网络服务用 Netty(封装了 NIO)。手写 NIO 容易出错,推荐用框架。
9. 对象序列化
序列化把对象转为字节流(持久化/网络传输),用 Serializable 标记接口。
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 版本号,反序列化校验
private String name;
private transient String password; // transient:不参与序列化
private int age;
public User(String name, String password, int age) {
this.name = name; this.password = password; this.age = age;
}
@Override public String toString() { return "User{name=" + name + ", pwd=" + password + ", age=" + age + "}"; }
}
// 序列化到文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
oos.writeObject(new User("Tom", "secret", 18));
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
User u = (User) ois.readObject();
System.out.println(u); // User{name=Tom, pwd=null, age=18} password 被跳过
}
Java 序列化的局限
- 跨语言不通用(只有 Java 能读)。
- 安全性差(反序列化可执行任意代码,历史上多次 RCE 漏洞)。
- 性能一般。
生产环境推荐 JSON(Jackson/Gson)、Protobuf、MessagePack 等替代。Java 原生序列化仅用于简单场景或 RMI。
serialVersionUID
声明 serialVersionUID 可保证类演进后反序列化兼容(字段增删不报错)。不声明则编译器按类结构自动生成,改动类会导致版本不一致、反序列化失败。
10. 综合案例:文件统计工具
综合 Files、缓冲流、流式处理,统计目录下所有 .java 文件的总行数:
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;
public class LineCounter {
public static void main(String[] args) throws IOException {
Path root = Path.of("src");
long totalLines;
long fileCount;
try (var files = Files.walk(root)) {
List<Path> javaFiles = files
.filter(p -> p.toString().endsWith(".java"))
.toList();
fileCount = javaFiles.size();
totalLines = 0;
for (Path f : javaFiles) {
try (var lines = Files.lines(f)) {
totalLines += lines.count();
}
}
}
System.out.printf("Java 文件数: %d%n", fileCount);
System.out.printf("总行数: %d%n", totalLines);
}
}
try-with-resources 嵌套流
Files.walk 和 Files.lines 返回的流持有系统资源(文件句柄),必须 close。用嵌套的 try-with-resources 确保都关闭。
小结
| 主题 | 关键点 |
|---|---|
| 字节流 | InputStream/OutputStream,处理二进制 |
| 字符流 | Reader/Writer,处理文本,自动编码 |
| 缓冲流 | BufferedXxx,批量读写,性能必备,装饰器模式 |
| 转换流 | InputStreamReader/OutputStreamWriter,指定编码 |
| Path/Files | NIO.2 现代 API,日常优先,流式读大文件 |
| NIO | Buffer/Channel/Selector,非阻塞,高并发,用 Netty |
| 序列化 | Serializable + transient,生产用 JSON/Protobuf 替代 |
| 资源管理 | 一律 try-with-resources |
下一篇进入泛型与注解:泛型类/方法/通配符、类型擦除、注解定义与处理。
下一篇预告
《08 - 泛型与注解》:泛型类/接口/方法、? extends/? super 与 PECS 原则、类型擦除原理、自定义注解与元注解、注解的实战应用。
