Appearance
Java IO 与 NIO 完整技术笔记
一、概述
一句话定义:Java IO 是 Java 语言中用于处理数据输入/输出的核心模块,涵盖传统阻塞 IO(BIO)、新 IO(NIO)和异步 IO(AIO)三代演进。
解决什么问题:程序与外部世界(文件系统、网络、设备)之间的数据交换。
适用场景对照:
| 模型 | 适用场景 | 典型框架/应用 |
|---|---|---|
| BIO | 连接数少、逻辑简单的场景(文件读写、小型服务) | 传统 Servlet(Tomcat BIO Connector) |
| NIO | 高并发、长连接、海量连接场景 | Netty、Mina、Tomcat NIO Connector、Kafka |
| AIO(NIO.2) | 连接数多且 IO 操作耗时长的场景 | 理论适合,但实际 Linux 下收益有限,使用较少 |
主体约定:IO 中的「输入/输出」以 计算机内存 为主体——数据进入内存叫输入(Input),从内存出去叫输出(Output)。
整体知识脉络:
mermaid
graph TB
A[Java IO 体系] --> B[传统 IO - BIO]
A --> C[New IO - NIO]
A --> D[NIO.2 / AIO - Java 7+]
B --> B1[字节流 InputStream/OutputStream]
B --> B2[字符流 Reader/Writer]
B --> B3[缓冲流 Buffered*]
B --> B4[对象序列化]
C --> C1[Channel 通道]
C --> C2[Buffer 缓冲区]
C --> C3[Selector 多路复用]
D --> D1[Path & Files API]
D --> D2[AsynchronousChannel]
D --> D3[WatchService]二、核心概念与原理
2.1 五种 UNIX IO 模型
在深入 Java IO 之前,必须理解操作系统层面的 IO 模型(以网络 IO 为例,一次 read 涉及两个阶段:①等待数据就绪 ②将数据从内核缓冲区复制到用户空间):
| IO 模型 | 阶段一(等待数据) | 阶段二(数据复制) | Java 对应 |
|---|---|---|---|
| 同步阻塞 IO(Blocking IO) | 阻塞 | 阻塞 | BIO(java.io) |
| 同步非阻塞 IO(Non-blocking IO) | 非阻塞(轮询) | 阻塞 | NIO(configureBlocking(false)) |
| IO 多路复用(Multiplexing) | 阻塞在 select/epoll | 阻塞 | NIO + Selector |
| 信号驱动 IO | 非阻塞(信号通知) | 阻塞 | Java 未直接支持 |
| 异步 IO(AIO) | 非阻塞 | 非阻塞(内核完成后回调) | AIO(java.nio.channels.Asynchronous*) |
关键理解:BIO 和 NIO 的本质区别在于阶段一是否阻塞。NIO 的 Selector 虽然
select()本身阻塞,但它可以用一个线程同时等待多个 Channel 的事件,这就是多路复用的价值。
2.2 BIO 的流模型 vs NIO 的 Channel/Buffer 模型
mermaid
graph LR
subgraph BIO - 基于流
A1[程序] -->|OutputStream 写| A2[外部设备]
A2 -->|InputStream 读| A1
end
subgraph NIO - 基于 Channel + Buffer
B1[程序] -->|写入 Buffer| B2[Buffer]
B2 -->|write| B3[Channel]
B3 -->|read| B4[Buffer]
B4 -->|读取 Buffer| B1
end| 对比维度 | BIO(Stream) | NIO(Channel + Buffer) |
|---|---|---|
| 数据方向 | 单向(InputStream 只读,OutputStream 只写) | 双向(Channel 可读可写) |
| 数据单位 | 逐字节 / 逐字符 | 面向缓冲区(Block) |
| 阻塞性 | 阻塞 | 可配置为非阻塞 |
| 多路复用 | 不支持(一个线程一个连接) | 支持(Selector) |
| API 复杂度 | 简单直观 | 较复杂(Buffer 状态管理) |
三、传统 IO(BIO)详解
3.1 四大基类
mermaid
graph TB
subgraph 字节流
IS[InputStream] --> FIS[FileInputStream]
IS --> BIS[BufferedInputStream]
IS --> DIS[DataInputStream]
IS --> OIS[ObjectInputStream]
IS --> BAIS[ByteArrayInputStream]
OS[OutputStream] --> FOS[FileOutputStream]
OS --> BOS[BufferedOutputStream]
OS --> DOS[DataOutputStream]
OS --> OOS[ObjectOutputStream]
end
subgraph 字符流
R[Reader] --> FR[FileReader]
R --> BR[BufferedReader]
R --> ISR[InputStreamReader]
W[Writer] --> FW[FileWriter]
W --> BW[BufferedWriter]
W --> OSW[OutputStreamWriter]
end选择原则:
- 文本数据(
.txt、.csv、.json、.xml)→ 字符流(Reader/Writer) - 二进制数据(图片、音频、视频、
.class文件)→ 字节流(InputStream/OutputStream)
3.2 字节流体系
3.2.1 核心实现类速查
| 类名 | 用途 | 关键点 |
|---|---|---|
FileInputStream / FileOutputStream | 文件读写 | 务必用 BufferedStream 包装 |
ByteArrayInputStream / ByteArrayOutputStream | 内存中的字节数组读写 | 适合测试、内存缓冲 |
BufferedInputStream / BufferedOutputStream | 带缓冲的装饰流 | 默认 8KB 缓冲区 |
DataInputStream / DataOutputStream | 读写 Java 基本类型 | 按固定字节数读写 int/long/double 等 |
ObjectInputStream / ObjectOutputStream | 对象序列化/反序列化 | 需实现 Serializable |
3.2.2 BufferedInputStream 缓冲机制(重点)
核心思想:将多次系统调用(read())合并为一次批量读取,减少用户态/内核态切换。
java
// ❌ 错误用法:逐字节读取,每次 read() 触发一次系统调用
try (FileInputStream fis = new FileInputStream("large_file.dat")) {
int b;
while ((b = fis.read()) != -1) {
// 处理字节 —— 极慢!
}
}
// ✅ 正确用法:用 BufferedInputStream 包装
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("large_file.dat"))) {
int b;
while ((b = bis.read()) != -1) {
// 实际上大多数 read() 从内存缓冲区取数据,不触发系统调用
}
}
// ✅✅ 更佳用法:批量读取到自定义数组
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("large_file.dat"))) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
// 处理 buffer[0..bytesRead-1]
}
}源码关键点(JDK 8):
java
public class BufferedInputStream extends FilterInputStream {
protected volatile byte buf[]; // 内部缓冲区
private static int DEFAULT_BUFFER_SIZE = 8192; // 默认 8KB
public synchronized int read() throws IOException {
if (pos >= count) { // 缓冲区已读完
fill(); // 从底层流批量填充缓冲区
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff; // 从缓冲区取一个字节
}
}性能对比(读取 100MB 文件):
| 方式 | 耗时(参考值) |
|---|---|
FileInputStream.read()(逐字节) | ~40s |
BufferedInputStream.read()(逐字节但有缓冲) | ~0.6s |
FileInputStream.read(byte[8192])(批量读) | ~0.3s |
BufferedInputStream.read(byte[8192]) | ~0.3s |
结论:如果你已经自己用
byte[]批量读取,BufferedInputStream的提升不明显;但如果代码逐字节读取,必须包装BufferedInputStream。
3.2.3 装饰器模式(Decorator Pattern)
Java IO 的设计是经典的装饰器模式范例:
mermaid
classDiagram
class InputStream {
<<abstract>>
+read() int
+read(byte[]) int
+close()
}
class FileInputStream {
+read() int
}
class FilterInputStream {
#InputStream in
+read() int
}
class BufferedInputStream {
-byte[] buf
+read() int
}
class DataInputStream {
+readInt() int
+readUTF() String
}
InputStream <|-- FileInputStream
InputStream <|-- FilterInputStream
FilterInputStream <|-- BufferedInputStream
FilterInputStream <|-- DataInputStream
FilterInputStream o-- InputStream : wraps层层包装示例:
java
// FileInputStream → BufferedInputStream → DataInputStream
// 底层数据源 → 添加缓冲能力 → 添加类型化读取能力
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")
)
);
int value = dis.readInt(); // 读取一个 int(4字节)
String text = dis.readUTF(); // 读取 UTF-8 字符串设计优势:
- 各层职责单一,可自由组合
- 扩展新功能无需修改已有类(如
GZIPInputStream、CipherInputStream) - 符合开闭原则
3.3 字符流与编码转换
3.3.1 桥梁类:InputStreamReader / OutputStreamWriter
java
// ✅ 显式指定 UTF-8 编码(推荐)
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// ❌ 不指定编码 —— 使用 JVM 默认编码(平台相关,可能产生乱码)
new InputStreamReader(new FileInputStream("data.txt")); // 危险!3.3.2 UTF-8 编码原理
UTF-8 是变长编码,使用 1~4 个字节表示一个 Unicode 码点:
| Unicode 范围 | UTF-8 字节数 | 二进制格式 | 示例 |
|---|---|---|---|
| U+0000 ~ U+007F | 1 字节 | 0xxxxxxx | A → 0x41 |
| U+0080 ~ U+07FF | 2 字节 | 110xxxxx 10xxxxxx | é → 0xC3 0xA9 |
| U+0800 ~ U+FFFF | 3 字节 | 1110xxxx 10xxxxxx 10xxxxxx | 中 → 0xE4 0xB8 0xAD |
| U+10000 ~ U+10FFFF | 4 字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 😀 → 0xF0 0x9F 0x98 0x80 |
常用字符编码对比:
| 编码 | 英文 | 中文 | 特点 |
|---|---|---|---|
| UTF-8 | 1 字节 | 3 字节 | 互联网标准,兼容 ASCII |
| Unicode(UTF-16) | 2 字节 | 2 字节 | Java char 内部编码 |
| GBK | 1 字节 | 2 字节 | Windows 中文系统常用 |
Java String 内部编码:JDK 8 使用
char[](UTF-16),JDK 9+ 引入 Compact Strings(byte[]+coder标记),Latin-1 字符只用 1 字节,节省约 50% 内存。
3.3.3 字符集乱码排查清单
mermaid
flowchart TD
A[出现乱码] --> B{确认文件实际编码}
B -->|使用 file 命令或 Hex 查看器| C{代码中是否显式指定编码?}
C -->|否| D[添加 StandardCharsets.UTF_8]
C -->|是| E{编码与文件实际编码一致?}
E -->|否| F[修正为一致的编码]
E -->|是| G{检查 JVM 默认编码}
G --> H[设置 -Dfile.encoding=UTF-8]
G --> I{检查数据库连接字符集}
I --> J[URL 加 characterEncoding=utf8]黄金法则:在所有 IO 边界(文件读写、网络传输、数据库连接)显式指定 StandardCharsets.UTF_8。
3.4 对象序列化与反序列化
3.4.1 基本用法
java
// 实体类
public class User implements Serializable {
private static final long serialVersionUID = 1L; // ⚠️ 必须显式声明
private String name;
private int age;
private transient String password; // transient:不参与序列化
// 构造方法、getter/setter 省略
}
// 序列化(写入)
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.ser"))) {
oos.writeObject(new User("张三", 25, "secret123"));
}
// 反序列化(读取)
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println(user.getName()); // "张三"
System.out.println(user.getPassword()); // null(transient 字段)
}3.4.2 安全风险与替代方案
| 方案 | 安全性 | 跨语言 | 数据大小 | 推荐度 |
|---|---|---|---|---|
| Java 原生序列化 | ❌ 存在反序列化漏洞 | ❌ | 大 | ⭐ |
| JSON(Jackson/Gson) | ✅ | ✅ | 中 | ⭐⭐⭐⭐⭐ |
| Protocol Buffers | ✅ | ✅ | 小 | ⭐⭐⭐⭐⭐ |
| Hessian | ✅ | 部分 | 中 | ⭐⭐⭐ |
安全建议:如果必须使用 Java 序列化,配合 JEP 290(JDK 9+)的反序列化过滤器(
ObjectInputFilter)进行白名单控制。
四、NIO(New IO)详解
4.1 三大核心组件
mermaid
graph LR
subgraph NIO 三大组件
S[Selector 选择器] -->|管理多个| C[Channel 通道]
C -->|数据读写通过| B[Buffer 缓冲区]
end
C1[SocketChannel] -.->|注册| S
C2[SocketChannel] -.->|注册| S
C3[ServerSocketChannel] -.->|注册| S4.2 Channel(通道)
Channel 是双向的数据传输通道,主要实现类:
| Channel 类型 | 用途 | 是否支持非阻塞 |
|---|---|---|
FileChannel | 文件读写 | ❌(始终阻塞) |
SocketChannel | TCP 客户端 | ✅ |
ServerSocketChannel | TCP 服务端 | ✅ |
DatagramChannel | UDP 通信 | ✅ |
4.3 Buffer(缓冲区)
4.3.1 三个核心属性
capacity(容量):缓冲区总大小,创建后不可变
position(位置):下一个要读/写的位置
limit(限制):可读/写的边界
不变式:0 ≤ position ≤ limit ≤ capacity4.3.2 Buffer 状态切换(重中之重)
mermaid
stateDiagram-v2
[*] --> 写模式: allocate()
写模式 --> 读模式: flip()
读模式 --> 写模式: clear() / compact()
note right of 写模式
position: 当前写入位置
limit = capacity
end note
note right of 读模式
position: 0(flip 后)
limit: 之前的 position
end note详细图解:
1. 初始状态 (allocate(10)):
position=0, limit=10, capacity=10
[_|_|_|_|_|_|_|_|_|_]
↑pos ↑limit=capacity
2. 写入 4 个字节 (put()):
position=4, limit=10, capacity=10
[A|B|C|D|_|_|_|_|_|_]
↑pos ↑limit
3. flip() 切换为读模式:
position=0, limit=4, capacity=10
[A|B|C|D|_|_|_|_|_|_]
↑pos ↑limit
4. 读取 2 个字节 (get()):
position=2, limit=4, capacity=10
[A|B|C|D|_|_|_|_|_|_]
↑pos ↑limit
5. compact() 压缩未读数据,切回写模式:
position=2, limit=10, capacity=10
[C|D|_|_|_|_|_|_|_|_]
↑pos ↑limit代码示例:
java
// ByteBuffer 基本操作
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 写入数据
buffer.put("Hello NIO".getBytes(StandardCharsets.UTF_8));
// 切换为读模式(重要!忘记 flip 是最常见的 NIO Bug)
buffer.flip();
// 读取数据
byte[] bytes = new byte[buffer.remaining()]; // remaining() = limit - position
buffer.get(bytes);
System.out.println(new String(bytes, StandardCharsets.UTF_8)); // "Hello NIO"
// 清空,准备下次写入
buffer.clear(); // position=0, limit=capacity(数据未清除,只是标记重置)
// 或
buffer.compact(); // 将未读数据移到开头,position=remaining, limit=capacity4.3.3 堆内 Buffer vs 直接内存(Direct Buffer)
java
// 堆内缓冲区(JVM 堆上分配)
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 直接内存缓冲区(堆外内存,OS 本地内存)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);| 对比维度 | Heap Buffer | Direct Buffer |
|---|---|---|
| 内存位置 | JVM 堆 | OS 本地内存(堆外) |
| 分配速度 | 快 | 慢(涉及 OS 系统调用) |
| GC 管理 | ✅ 受 GC 管理 | ❌ 不受 GC 直接管理 |
| IO 性能 | 较低(需内核→堆拷贝) | 较高(减少一次拷贝) |
| 适用场景 | 临时、小量数据 | 长期存活、大量 IO(如网络框架) |
Direct Buffer IO 优势原理:
Heap Buffer IO 路径(4 次拷贝):
磁盘 → 内核缓冲区 → JVM 堆外临时区 → JVM 堆 Buffer → 应用程序
Direct Buffer IO 路径(3 次拷贝):
磁盘 → 内核缓冲区 → Direct Buffer(堆外)→ 应用程序
减少了「堆外临时区 → JVM 堆」这次拷贝最佳实践:Direct Buffer 分配昂贵,应池化复用(Netty 的
PooledByteBufAllocator就是这么做的)。通过-XX:MaxDirectMemorySize控制上限,避免堆外 OOM。
4.3.4 零拷贝:FileChannel.transferTo()
java
// 传统方式:文件 → 网络(4 次数据拷贝 + 4 次上下文切换)
FileInputStream fis = new FileInputStream("large_file.dat");
SocketOutputStream sos = socket.getOutputStream();
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
sos.write(buffer, 0, len); // 数据经过用户空间中转
}
// 零拷贝方式:数据不经过用户空间(2 次拷贝 + 2 次上下文切换)
FileChannel fileChannel = new FileInputStream("large_file.dat").getChannel();
SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("localhost", 8080));
// 利用 OS 的 sendfile 系统调用
fileChannel.transferTo(0, fileChannel.size(), socketChannel);零拷贝数据流向对比:
传统拷贝(4 次):
磁盘 → 内核页缓存 → 用户空间 buffer → socket 内核缓冲区 → 网卡
零拷贝 sendfile(2 次):
磁盘 → 内核页缓存 → 网卡(DMA 直传,跳过用户空间)应用场景:Kafka 消费消息时使用
FileChannel.transferTo()将日志文件直接发送到网络,是其高吞吐的关键技术之一。
4.4 Selector 多路复用
4.4.1 工作流程
mermaid
sequenceDiagram
participant App as 应用程序
participant Sel as Selector
participant CH1 as Channel 1
participant CH2 as Channel 2
participant CH3 as Channel 3
App->>Sel: Selector.open()
CH1->>Sel: register(selector, OP_ACCEPT)
CH2->>Sel: register(selector, OP_READ)
CH3->>Sel: register(selector, OP_READ)
loop 事件循环
App->>Sel: select() [阻塞等待就绪事件]
Sel-->>App: 返回就绪的 SelectionKey 集合
App->>App: 遍历 keys,处理就绪的 Channel
end4.4.2 NIO Server 完整示例
java
public class NioEchoServer {
public static void main(String[] args) throws IOException {
// 1. 创建 Selector
Selector selector = Selector.open();
// 2. 创建 ServerSocketChannel 并配置非阻塞
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(8080));
// 3. 注册 ACCEPT 事件到 Selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Echo Server started on port 8080");
// 4. 事件循环
while (true) {
selector.select(); // 阻塞直到至少一个事件就绪
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // ⚠️ 必须手动移除,否则下次还会处理
if (key.isAcceptable()) {
// 处理新连接
handleAccept(key, selector);
} else if (key.isReadable()) {
// 处理读事件
handleRead(key);
}
}
}
}
private static void handleAccept(SelectionKey key, Selector selector)
throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 新连接注册 READ 事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("New connection: " + clientChannel.getRemoteAddress());
}
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端断开连接
key.cancel();
clientChannel.close();
return;
}
buffer.flip(); // ⚠️ 切换为读模式
// Echo: 将收到的数据原样写回
clientChannel.write(buffer);
}
}4.4.3 epoll 原理与触发模式
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大连接数 | 1024(FD_SETSIZE) | 无限制 | 无限制 |
| 时间复杂度 | $O(n)$ 轮询 | $O(n)$ 轮询 | $O(1)$ 事件通知 |
| 数据结构 | bitmap | 链表 | 红黑树 + 就绪链表 |
| 内存拷贝 | 每次全量拷贝 fd 集合 | 每次全量拷贝 | epoll_ctl 只增量修改 |
| 触发模式 | LT | LT | LT + ET |
两种触发模式:
| 模式 | 行为 | 优点 | 缺点 |
|---|---|---|---|
| LT(水平触发) | 只要缓冲区有数据就持续通知 | 编程简单,不易丢数据 | 可能重复通知,性能略低 |
| ET(边缘触发) | 仅在数据到达瞬间通知一次 | 减少通知次数,性能更高 | 必须一次读完所有数据(循环 read 直到 EAGAIN) |
Netty 默认使用 ET 模式(
EpollSocketChannel),并在内部处理了 ET 模式的复杂性。Java 原生 NIO 的 Selector 在 Linux 上使用 LT 模式的 epoll。
4.4.4 Reactor 模式与 Netty
mermaid
graph TB
subgraph "主从 Reactor 模型(Netty 架构)"
C[客户端连接] --> MR[MainReactor<br/>Boss EventLoopGroup<br/>处理 ACCEPT]
MR -->|新连接分发| SR1[SubReactor 1<br/>Worker EventLoop<br/>处理 READ/WRITE]
MR -->|新连接分发| SR2[SubReactor 2<br/>Worker EventLoop<br/>处理 READ/WRITE]
MR -->|新连接分发| SR3[SubReactor N<br/>Worker EventLoop<br/>处理 READ/WRITE]
SR1 --> H1[ChannelHandler<br/>Pipeline]
SR2 --> H2[ChannelHandler<br/>Pipeline]
SR3 --> H3[ChannelHandler<br/>Pipeline]
endReactor 模式 → Netty 组件映射:
| Reactor 概念 | Netty 实现 |
|---|---|
| MainReactor | BossEventLoopGroup(通常 1 个线程) |
| SubReactor | WorkerEventLoopGroup(通常 CPU 核数 × 2 个线程) |
| Acceptor | ServerBootstrap.bind() |
| Handler | ChannelHandler(ChannelInboundHandler / ChannelOutboundHandler) |
| 事件循环 | EventLoop(内部封装了 Selector) |
4.5 NIO.2:Files 与 Path(Java 7+)
4.5.1 Path vs File
java
// 旧 API(java.io.File)
File file = new File("/home/user/data.txt");
// 新 API(java.nio.file.Path)—— 推荐
Path path = Paths.get("/home/user/data.txt");
// 或 Java 11+
Path path2 = Path.of("/home", "user", "data.txt");
// 路径操作
path.resolve("subdir/file.txt"); // 拼接子路径
path.getParent(); // 父目录
path.getFileName(); // 文件名
path.normalize(); // 规范化(消除 ../ 和 ./)
path.toAbsolutePath(); // 转绝对路径4.5.2 Files 工具类常用操作
java
// 读取文件全部内容
byte[] bytes = Files.readAllBytes(Path.of("data.bin"));
String content = Files.readString(Path.of("data.txt")); // Java 11+
List<String> lines = Files.readAllLines(Path.of("data.txt"), StandardCharsets.UTF_8);
// 写入文件
Files.write(Path.of("output.txt"), "Hello".getBytes(),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
Files.writeString(Path.of("output.txt"), "Hello World"); // Java 11+
// 文件操作
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
Files.delete(path);
Files.createDirectories(Path.of("/a/b/c")); // 递归创建目录
// 文件属性
boolean exists = Files.exists(path);
long size = Files.size(path);
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);4.5.3 Files.walk() 大目录遍历
java
// ⚠️ 必须用 try-with-resources(Stream 实现了 AutoCloseable)
try (Stream<Path> stream = Files.walk(Path.of("D:\\"), 3)) { // maxDepth=3
stream.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().equals("target.mp4"))
.forEach(p -> System.out.println("Found: " + p));
}
// Files.find() 更高效(过滤在遍历过程中执行,而非后过滤)
try (Stream<Path> stream = Files.find(Path.of("D:\\"), Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile()
&& path.getFileName().toString().endsWith(".mp4"))) {
stream.forEach(System.out::println);
}对比输入内容中的递归遍历方案:输入内容中使用
File.listFiles()递归遍历 D 盘(约 300GB),耗时约 8.5 秒。使用Files.walk()可以达到类似性能,但代码量大幅减少,且懒加载特性不会将所有路径加载到内存。
五、对比与易混淆点
5.1 BIO vs NIO vs AIO 全面对比
| 维度 | BIO | NIO | AIO(NIO.2) |
|---|---|---|---|
| Java 版本 | JDK 1.0 | JDK 1.4 | JDK 7 |
| 编程模型 | 面向流(Stream) | 面向缓冲(Buffer) | 面向回调(CompletionHandler) |
| 阻塞性 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
| 线程模型 | 1 连接 = 1 线程 | 1 线程管理 N 连接 | OS 内核完成后回调 |
| 吞吐量 | 低 | 高 | 理论最高 |
| 编程复杂度 | ★☆☆ | ★★★ | ★★★★ |
| Linux 底层 | read()/write() | epoll | io_uring(较新)/ 模拟实现 |
| 实际应用 | 小型应用 | Netty、Kafka、RocketMQ | 较少(Linux 下 AIO 实现不成熟) |
5.2 InputStream vs Channel
| 对比 | InputStream(BIO) | Channel(NIO) |
|---|---|---|
| 方向 | 单向(只读或只写) | 双向(读写均可) |
| 数据处理 | 直接操作字节/字符 | 必须经过 Buffer |
| 阻塞 | 始终阻塞 | 可配置非阻塞 |
| 关联 Selector | ❌ | ✅ |
5.3 clear() vs compact() vs flip()
| 方法 | 作用 | position | limit | 使用时机 |
|---|---|---|---|---|
flip() | 写→读切换 | 0 | 原 position | 写完数据后准备读取 |
clear() | 读→写切换(丢弃所有) | 0 | capacity | 数据全部读完,准备重新写入 |
compact() | 读→写切换(保留未读) | remaining | capacity | 数据未读完,保留剩余数据继续写入 |
六、最佳实践与常见坑
6.1 BIO 最佳实践
- 始终在 try-with-resources 中使用流
java
// ✅ 正确
try (InputStream is = new FileInputStream("data.txt");
BufferedInputStream bis = new BufferedInputStream(is)) {
// 使用 bis
} // 自动关闭,即使抛异常
// ❌ 错误:手动 close 容易遗漏
InputStream is = new FileInputStream("data.txt");
// ... 如果这里抛异常,is 永远不会被关闭
is.close();文件读写始终指定字符编码(
StandardCharsets.UTF_8)不要在生产环境使用 Java 原生序列化,改用 JSON 或 Protobuf
显式声明
serialVersionUID,避免类修改后反序列化失败
6.2 NIO 常见坑
- 忘记调用
flip()——这是 NIO 编程中最常见的 Bug
java
buffer.put(data);
// buffer.flip(); // ❌ 忘记 flip,下面 write 将写入空数据
channel.write(buffer);- SelectionKey 未手动移除
java
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // ⚠️ 必须移除!否则下一次 select() 仍会返回已处理的 key
// 处理事件...
}Direct Buffer 内存泄漏
- Direct Buffer 不受 GC 直接管理,需要通过
Cleaner或手动释放 - 设置
-XX:MaxDirectMemorySize防止堆外 OOM - 生产环境应使用池化 Buffer(如 Netty 的
PooledByteBufAllocator)
- Direct Buffer 不受 GC 直接管理,需要通过
FileChannel不支持非阻塞模式——不能注册到 Selector半包/粘包问题:TCP 是流式协议,一次
read()可能读到半个消息或多个消息,需要应用层处理消息边界(长度前缀 / 分隔符 / 固定长度)
6.3 文件操作最佳实践
| 需求 | 推荐 API | 说明 |
|---|---|---|
| 简单读写小文件 | Files.readString() / Files.writeString() (Java 11+) | 最简洁 |
| 读大文件 | Files.newBufferedReader() + 逐行处理 | 避免 OOM |
| 文件拷贝 | Files.copy() | 底层可能使用零拷贝 |
| 遍历目录 | Files.walk() / Files.find() | 懒加载,注意 try-with-resources |
| 监控文件变化 | WatchService | 配置热更新场景 |
七、面试高频问题
Q1:BIO、NIO、AIO 的区别是什么?
答:三者本质区别在于 IO 操作的阻塞方式:
- BIO:同步阻塞,线程在
read()/accept()时阻塞,直到有数据/连接。一个连接需要一个线程,并发能力差。 - NIO:同步非阻塞,基于 Channel/Buffer/Selector。通过 Selector 多路复用,一个线程可监控多个 Channel 的 IO 事件,适合高并发场景。
- AIO:异步非阻塞,IO 操作由 OS 内核完成后通过回调通知应用程序,应用线程无需等待。但 Linux 下 AIO 实现不成熟,实际使用较少。
Q2:NIO 中 Buffer 的 flip()、clear()、compact() 分别做什么?
答:
flip():写模式→读模式。limit = position; position = 0,准备读取之前写入的数据。clear():读模式→写模式。position = 0; limit = capacity,清空标记(数据未擦除)准备重新写入。compact():读模式→写模式。将未读数据移到缓冲区头部,position = remaining; limit = capacity,适合数据未完全读完的场景。
Q3:什么是零拷贝?Java 中如何实现?
答:零拷贝是指数据传输过程中避免 CPU 在用户空间与内核空间之间拷贝数据。Java 中通过两种方式实现:
FileChannel.transferTo():底层调用 Linuxsendfile()系统调用,数据直接从内核页缓存传输到网卡缓冲区,不经过用户空间。MappedByteBuffer(mmap):将文件映射到进程虚拟内存,读写文件如同读写内存。
Kafka 的高吞吐量和 Netty 的高性能都大量使用了零拷贝技术。
Q4:Netty 为什么不直接使用 Java NIO?
答:Java 原生 NIO 存在以下问题:
- API 复杂,Buffer 状态管理容易出错(如忘记 flip)
- Selector 在 Linux 上存在 epoll 空轮询 bug(JDK Bug 6670302),导致 CPU 100%
- 没有完善的半包/粘包处理
- 没有内置的心跳、重连、超时机制
- 没有 Buffer 池化,Direct Buffer 管理困难
Netty 封装了 Reactor 模式,解决了以上所有问题,并提供了 PooledByteBufAllocator、ChannelPipeline、EventLoop 等高级抽象。
Q5:Java 中如何避免字符编码乱码?
答:核心原则——在所有 IO 边界显式指定 StandardCharsets.UTF_8:
- 文件读写:
new InputStreamReader(is, StandardCharsets.UTF_8) - 网络传输:统一约定 UTF-8
- 数据库连接:URL 加
characterEncoding=utf8 - JVM 参数:
-Dfile.encoding=UTF-8(作为兜底) - 确认数据源本身的编码与代码中指定的一致
八、总结
┌─────────────────────────────────────────────────────────────┐
│ Java IO 知识体系总结 │
├─────────────────────────────────────────────────────────────┤
│ │
│ BIO(传统 IO) │
│ ├─ 字节流:InputStream / OutputStream │
│ ├─ 字符流:Reader / Writer(注意编码,统一 UTF-8) │
│ ├─ 缓冲流:BufferedXxx(8KB 缓冲,减少系统调用) │
│ ├─ 设计模式:装饰器模式(层层包装,职责单一) │
│ └─ 序列化:优先 JSON/Protobuf,避免 Java 原生序列化 │
│ │
│ NIO(New IO) │
│ ├─ Channel:双向通道(FileChannel / SocketChannel) │
│ ├─ Buffer:缓冲区(flip/clear/compact 状态切换) │
│ │ ├─ Heap Buffer vs Direct Buffer │
│ │ └─ 零拷贝:transferTo() → sendfile 系统调用 │
│ ├─ Selector:多路复用(一个线程管理 N 连接) │
│ │ └─ 底层:epoll (Linux) / kqueue (Mac) │
│ └─ Reactor 模式 → Netty(Boss/Worker EventLoopGroup) │
│ │
│ NIO.2(Java 7+) │
│ ├─ Path + Files:现代文件操作 API │
│ ├─ Files.walk():懒加载目录遍历(必须 try-with-resources) │
│ └─ WatchService:文件变更监控 │
│ │
│ 核心原则 │
│ ├─ IO 边界显式指定 UTF-8 编码 │
│ ├─ 流必须在 try-with-resources 中使用 │
│ ├─ 大文件使用 BufferedStream 或 NIO Channel │
│ └─ 高并发网络场景使用 NIO(Netty)而非 BIO │
│ │
└─────────────────────────────────────────────────────────────┘