NIO 简介
概述
NIO(New I/O) 自 JDK 1.4 引入,提供了一套面向缓冲区(Buffer)和通道(Channel)的 IO 模型,与传统基于流(Stream)的 字节流、字符流 形成互补。NIO 支持块(block)式读写、非阻塞 IO 以及多路复用(Selector),适合高并发、大文件或网络 IO 场景。本文介绍 NIO 的核心概念与基本用法,为后续学习网络编程或高性能 IO 打基础。
说明
「NIO」常指 java.nio 包下的 Buffer、Channel、Selector 等;NIO.2(Java 7)在 java.nio.file 中增加了 Path、Files、FileChannel 的增强等,本文会简要提及。
NIO 与传统 IO 的对比
| 维度 | 传统 IO(Stream) | NIO |
|---|---|---|
| 数据单位 | 面向流,逐字节或字节数组 | 面向块,以 Buffer 为单位读写 |
| 方向 | 单向(InputStream/OutputStream 分开) | Channel 可读可写(取决于实现) |
| 阻塞 | 默认阻塞:read/write 会阻塞线程 | 支持非阻塞模式 + Selector 多路复用 |
| 典型用法 | 顺序读写、简单文件/网络 | 大文件、高并发、需要多路复用时的网络 IO |
学习 NIO 时,重点理解 Buffer 与 Channel 的配合:数据从 Channel 读入 Buffer,或从 Buffer 写出到 Channel;Buffer 负责在内存中维护读写位置与界限。
核心概念:Buffer 与 Channel
Buffer(缓冲区)
Buffer 是一块连续内存,用于暂存要读入或写出的数据。最常用的是 ByteBuffer,另有 CharBuffer、IntBuffer 等。
Buffer 有三个关键属性(以写后读为例):
| 属性 | 含义 |
|---|---|
| capacity | 容量,创建时固定 |
| position | 当前读写位置,随 get/put 移动 |
| limit | 可读写的边界;写模式中 limit = capacity,读模式中 limit = 上次写到的 position |
常用方法:
- flip():写模式 → 读模式。将
limit = position,position = 0,便于从刚写入的数据开头读。 - clear():清空为写模式。
position = 0,limit = capacity(不擦除数据,只是重置可写区域)。 - rewind():
position = 0,不改 limit,用于重新读一遍。
提示
写数据后若要「从刚写的内容里读」,必须先 flip();读完若要再次写入同一 Buffer,可 clear() 或 compact()(保留未读数据并转为写模式)。
Channel(通道)
Channel 表示与数据源/目标的连接,可从通道读入 Buffer 或把 Buffer 写出到通道。文件读写常用 FileChannel,通过 FileInputStream/FileOutputStream/RandomAccessFile.getChannel() 获得。Channel 通常需配合 Buffer 使用。
基本用法
创建 ByteBuffer
// 堆上分配,容量 1024 字节
ByteBuffer buf = ByteBuffer.allocate(1024);
// 直接内存(堆外),适合与 Channel 大量交换数据,减少拷贝
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024);从 FileChannel 读取到 Buffer
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class NioReadExample {
public static void main(String[] args) {
Path path = Path.of("data/hello.txt");
// try-with-resources 可同时管理 FileChannel(实现了 AutoCloseable)
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(256);
int len;
while ((len = channel.read(buf)) != -1) {
buf.flip(); // 切换为读模式
// 处理 buf 中 0..len 的数据,例如解码为字符串
byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
System.out.print(new String(bytes));
buf.clear(); // 清空以便下次读
}
} catch (IOException e) {
System.err.println("读取失败: " + e.getMessage());
}
}
}从 Buffer 写入 FileChannel
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class NioWriteExample {
public static void main(String[] args) {
Path path = Path.of("data/out.txt");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
String text = "Hello, NIO!\n";
ByteBuffer buf = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
// 写出时 channel.write() 会从 buf 的 position 读到 limit
while (buf.hasRemaining()) {
channel.write(buf);
}
} catch (IOException e) {
System.err.println("写入失败: " + e.getMessage());
}
}
}ByteBuffer.wrap(byte[]) 会包装现有字节数组,position=0,limit=length,无需单独 allocate。
使用示例
示例 1:复制文件(Buffer + FileChannel)
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class NioCopyFile {
public static void copy(Path src, Path dest) throws IOException {
try (FileChannel in = FileChannel.open(src, StandardOpenOption.READ);
FileChannel out = FileChannel.open(dest,
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
ByteBuffer buf = ByteBuffer.allocate(8192);
while (in.read(buf) != -1) {
buf.flip();
out.write(buf);
buf.clear();
}
}
}
}示例 2:使用 NIO.2 的 Files 简化读写(Java 7+)
NIO.2 的 Files 类提供了便捷的读写方法,内部会使用 NIO,但 API 更简单:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
// 读所有字节
byte[] bytes = Files.readAllBytes(Path.of("data/hello.txt"));
// 按行读取(返回 List<String>)
Files.readAllLines(Path.of("data/hello.txt"), StandardCharsets.UTF_8);
// 写字节或字符串
Files.write(Path.of("data/out.txt"), "Hello".getBytes(StandardCharsets.UTF_8));
Files.writeString(Path.of("data/out.txt"), "Hello", StandardCharsets.UTF_8); // JDK 11+小文件或简单场景可直接用 Files;需要流式、大文件或精细控制时,用 Buffer + Channel。
注意事项
注意
使用 Buffer 时务必注意 flip/clear/rewind 的时机:写完后要读必须先 flip();读完后要再写需 clear() 或 compact(),否则 position/limit 错乱会导致读不到数据或覆盖错误。
注意
FileChannel 通过 FileChannel.open() 或从流/RAF 的 getChannel() 获得,使用完毕后应关闭。用 try-with-resources 可自动关闭,避免资源泄漏。详见 异常处理 - try-with-resources。
提示
- allocateDirect() 分配的堆外内存适合与 Channel 大量交换数据,但创建和销毁成本较高,一般用于长期存在、大块读写的 Buffer。
- 网络 IO 中的 SocketChannel、ServerSocketChannel 与 Selector 结合可实现非阻塞多路复用,属于进阶内容,可在掌握 FileChannel 与 Buffer 后再学。
相关链接
- 字节流 — 传统字节流 IO
- 缓冲流与包装流 — 缓冲概念与 NIO Buffer 的前置
- File 类与文件操作 — 传统 File 与路径
- 异常处理 — try-with-resources 与受检异常
- Oracle Java 教程 - NIO