IO 与 NIO
体系概览
java.io(BIO,阻塞,面向流)
├── 字节流
│ ├── InputStream
│ │ ├── FileInputStream
│ │ ├── BufferedInputStream
│ │ ├── ByteArrayInputStream
│ │ └── ObjectInputStream
│ └── OutputStream
│ ├── FileOutputStream
│ ├── BufferedOutputStream
│ ├── ByteArrayOutputStream
│ └── ObjectOutputStream
└── 字符流
├── Reader
│ ├── FileReader
│ ├── BufferedReader
│ └── InputStreamReader(字节→字符转换)
└── Writer
├── FileWriter
├── BufferedWriter
└── OutputStreamWriter(字符→字节转换)
java.nio(NIO,非阻塞,面向缓冲区)
├── Channel(双向通道)
├── Buffer(缓冲区)
├── Selector(多路复用)
└── Path / Files(现代文件操作 API)
字节流
文件读写
// 读文件
try (InputStream in = new BufferedInputStream(
new FileInputStream("input.txt"))) {
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
process(buf, 0, len);
}
}
// 写文件(追加模式第二个参数传 true)
try (OutputStream out = new BufferedOutputStream(
new FileOutputStream("output.txt"))) {
out.write("hello".getBytes(StandardCharsets.UTF_8));
out.flush();
}
// 文件复制
try (InputStream in = new FileInputStream("src.txt");
OutputStream out = new FileOutputStream("dst.txt")) {
in.transferTo(out); // Java 9+,简洁高效
}ByteArrayOutputStream(内存流)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write("hello".getBytes());
baos.write(" world".getBytes());
byte[] result = baos.toByteArray();
String str = baos.toString(StandardCharsets.UTF_8);字符流
// 读文本文件(逐行)
try (BufferedReader reader = new BufferedReader(
new FileReader("input.txt", StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// 写文本文件
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("output.txt", StandardCharsets.UTF_8))) {
writer.write("第一行");
writer.newLine();
writer.write("第二行");
}
// 字节流 → 字符流(指定编码,推荐)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("input.txt"), StandardCharsets.UTF_8))) {
reader.lines().forEach(System.out::println);
}PrintWriter / PrintStream
// PrintWriter:方便的文本输出
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("out.txt")))) {
pw.println("hello");
pw.printf("name=%s, age=%d%n", "Alice", 30);
}NIO:Path 与 Files(推荐优先使用)
Java 7 引入的现代文件操作 API,比传统 IO 更简洁。
Path
Path path = Path.of("src/main/resources/data.txt"); // Java 11+
Path path2 = Paths.get("src", "main", "resources", "data.txt");
path.getFileName(); // data.txt
path.getParent(); // src/main/resources
path.getRoot(); // / 或 C:\
path.toAbsolutePath(); // 绝对路径
path.normalize(); // 消除 . 和 ..
path.resolve("other.txt"); // 拼接路径
path.relativize(other); // 相对路径
path.toFile(); // 转为 File 对象Files 工具类
Path path = Path.of("data.txt");
// 读写(小文件)
String content = Files.readString(path, StandardCharsets.UTF_8); // Java 11+
byte[] bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
Files.writeString(path, "内容", StandardCharsets.UTF_8); // Java 11+
Files.write(path, bytes);
Files.write(path, lines, StandardCharsets.UTF_8,
StandardOpenOption.APPEND); // 追加
// 流式读取(大文件,逐行处理,自动关闭)
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.filter(l -> l.contains("ERROR"))
.forEach(System.out::println);
}
// 复制 / 移动 / 删除
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);
Files.delete(path);
Files.deleteIfExists(path);
// 创建
Files.createFile(path);
Files.createDirectory(path);
Files.createDirectories(path); // 递归创建
Path tmp = Files.createTempFile("prefix", ".txt");
// 判断
Files.exists(path);
Files.notExists(path);
Files.isRegularFile(path);
Files.isDirectory(path);
Files.isReadable(path);
Files.size(path); // 文件大小(字节)
// 遍历目录
try (Stream<Path> entries = Files.list(path)) { // 当前目录
entries.filter(Files::isRegularFile).forEach(System.out::println);
}
try (Stream<Path> all = Files.walk(path, 3)) { // 递归,最大深度 3
all.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// 监听属性
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.creationTime();
attrs.lastModifiedTime();
attrs.size();NIO:Channel 与 Buffer
适合需要精细控制或高性能的场景。
FileChannel
// 读文件
try (FileChannel channel = FileChannel.open(
Path.of("data.txt"), StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(4096);
while (channel.read(buf) != -1) {
buf.flip(); // 切换为读模式
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear(); // 切换为写模式
}
}
// 写文件
try (FileChannel channel = FileChannel.open(
Path.of("out.txt"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
channel.write(buf);
}
// 零拷贝传输(高性能文件复制)
try (FileChannel src = FileChannel.open(Path.of("src.txt"), StandardOpenOption.READ);
FileChannel dst = FileChannel.open(Path.of("dst.txt"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
src.transferTo(0, src.size(), dst); // 内核层直接传输,不经用户空间
}ByteBuffer 操作
ByteBuffer buf = ByteBuffer.allocate(1024); // 堆内存
ByteBuffer direct = ByteBuffer.allocateDirect(1024); // 直接内存(减少一次拷贝)
// 写数据(position 移动)
buf.put("hello".getBytes());
buf.putInt(42);
// 切换读模式:limit = position, position = 0
buf.flip();
// 读数据
byte[] data = new byte[buf.remaining()];
buf.get(data);
// 重置:position = 0,limit 不变(可重复读)
buf.rewind();
// 清空:position = 0,limit = capacity(准备重新写)
buf.clear();
// 压缩:未读数据移到开头,position = 剩余字节数(保留未读继续写)
buf.compact();
// 标记
buf.mark(); // 记录当前 position
buf.reset(); // 恢复到 mark 位置内存映射文件(MappedByteBuffer)
大文件读写,将文件直接映射到内存,性能极高:
try (FileChannel channel = FileChannel.open(
Path.of("large.dat"), StandardOpenOption.READ, StandardOpenOption.WRITE)) {
MappedByteBuffer mbb = channel.map(
FileChannel.MapMode.READ_WRITE, 0, channel.size());
mbb.put(0, (byte) 42); // 直接修改,自动同步到文件
}关于
transferTo、MappedByteBuffer、DirectByteBuffer背后的内核拷贝原理(sendfile、mmap、DMA gather),见 零拷贝。
WatchService(目录监听)
WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Path.of("/tmp/watch");
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = watcher.take(); // 阻塞等待事件
for (WatchEvent<?> event : key.pollEvents()) {
Path changed = (Path) event.context();
System.out.println(event.kind() + ": " + changed);
}
key.reset(); // 必须重置,否则不再接收事件
}IO 模型
BIO / NIO / AIO 对比
| 模型 | 阻塞 | 线程模型 | 适用场景 |
|---|---|---|---|
| BIO(同步阻塞) | read/write 阻塞直到完成 | 一连接一线程 | 连接数少、开发简单 |
| NIO(同步非阻塞) | 立即返回,由 Selector 通知就绪 | 单线程管理多连接 | 高并发、长连接 |
| AIO(异步非阻塞) | 操作完成后回调通知 | 无需轮询,OS 驱动回调 | 超高并发、Linux 支持有限 |
NIO 的非阻塞本质:将 SocketChannel 设为非阻塞后,read() 没有数据时立即返回 0 而不挂起线程,再由 Selector 集中监听多个 Channel 的就绪事件。
select / poll / epoll
Linux 内核提供三种多路复用系统调用,Java NIO 在不同平台会选用最优实现:
| select | poll | epoll | |
|---|---|---|---|
| 数据结构 | fd_set(位图) | pollfd 数组 | 红黑树 + 就绪链表 |
| 最大连接数 | 1024(FD_SETSIZE) | 无限制 | 无限制 |
| 事件通知 | 遍历全部 fd | 遍历全部 fd | 只遍历就绪 fd(O(1)) |
| 内核拷贝 | 每次调用拷贝 fd 集合 | 每次调用拷贝 pollfd | 注册一次,无重复拷贝 |
| 触发方式 | 水平触发 | 水平触发 | 水平触发 / 边缘触发 |
Java NIO 在 Linux 上自动使用 epoll(JDK 1.5+
EPollSelectorProvider),macOS 使用 kqueue,Windows 使用 IOCP。
NIO 网络编程
SocketChannel(非阻塞客户端)
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 切换为非阻塞
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
// 非阻塞 connect 可能未立即完成,需轮询
while (!channel.finishConnect()) {
// 可做其他事情
}
ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
channel.write(buf);
ByteBuffer resp = ByteBuffer.allocate(1024);
int n = channel.read(resp); // 无数据时返回 0,不阻塞
if (n > 0) {
resp.flip();
System.out.println(StandardCharsets.UTF_8.decode(resp));
}
channel.close();Selector(多路复用核心)
// 创建 Selector
Selector selector = Selector.open();
// 注册感兴趣的事件(返回 SelectionKey)
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
key.attach(someContext); // 可附加业务对象
// 事件循环
while (true) {
int readyCount = selector.select(1000); // 阻塞至多 1 秒,有事件立即返回
if (readyCount == 0) continue;
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey sk = it.next();
it.remove(); // 必须手动移除,否则重复处理
if (sk.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) sk.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
SocketChannel client = (SocketChannel) sk.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int n = client.read(buf);
if (n == -1) {
sk.cancel();
client.close();
} else {
buf.flip();
client.write(buf); // echo 回去
}
}
}
}SelectionKey 四种事件:
| 常量 | 值 | 含义 |
|---|---|---|
OP_ACCEPT | 16 | ServerSocketChannel 有新连接 |
OP_CONNECT | 8 | SocketChannel 连接完成 |
OP_READ | 1 | Channel 有数据可读 |
OP_WRITE | 4 | Channel 可写(缓冲区未满) |
OP_WRITE通常不要一直注册,只在写缓冲区满时注册、写完后立即取消,否则select()会持续返回。
完整 NIO Echo Server
public class NioEchoServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Echo server started on :8080");
while (true) {
selector.select();
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
try {
if (key.isAcceptable()) {
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int n = client.read(buf);
if (n == -1) {
key.cancel();
client.close();
} else {
buf.flip();
client.write(buf);
}
}
} catch (IOException e) {
key.cancel();
key.channel().close();
}
}
}
}
}IO vs NIO 选型
| 场景 | 推荐 |
|---|---|
| 简单文件读写(小文件) | Files.readString / Files.writeString |
| 逐行处理文本 | Files.lines |
| 大文件顺序读写 | BufferedInputStream / FileChannel |
| 大文件随机访问 | FileChannel + ByteBuffer |
| 超大文件(GB 级) | MappedByteBuffer |
| 网络通信高并发 | SocketChannel + Selector |
| 目录变化监听 | WatchService |
编码注意
// 始终显式指定字符集,不依赖系统默认编码
Files.readString(path, StandardCharsets.UTF_8);
new InputStreamReader(in, StandardCharsets.UTF_8);
new String(bytes, StandardCharsets.UTF_8);
"hello".getBytes(StandardCharsets.UTF_8);