跳到主要内容

Java IO 与 NIO

Java 提供了丰富的 I/O(输入/输出)操作 API。从 Java 1.4 开始,引入了 NIO(New I/O),提供了更高效的 I/O 操作方式。Java 7 又引入了 NIO.2,进一步完善了文件操作 API。

IO 与 NIO 的核心区别

理解 IO 和 NIO 的本质区别,是选择合适技术方案的基础。

IO:面向流的阻塞式模型

传统 IO 采用**面向流(Stream-Oriented)**的设计,数据像水流一样单向流动:

输入流:数据源 ──────▶ 程序
输出流:程序 ──────▶ 数据目标

特点

  • 阻塞式:调用 read()write() 时,线程会阻塞直到操作完成
  • 单向流动:输入流只能读,输出流只能写,需要分别创建
  • 无缓冲:数据直接从流中读取或写入,每次操作都可能涉及系统调用

适用场景:连接数较少、每个连接数据量较大的场景

NIO:面向缓冲区的非阻塞模型

NIO 采用**面向缓冲区(Buffer-Oriented)**的设计:

程序 ◀──▶ 缓冲区(Buffer) ◀──▶ 通道(Channel) ◀──▶ 数据源/目标

特点

  • 非阻塞式:线程可以发起请求后立即返回,通过轮询或事件通知获取结果
  • 双向通道:Channel 可以同时进行读写操作
  • 缓冲区:数据先读入缓冲区,可以灵活地在缓冲区中移动、处理
  • 选择器:一个线程可以管理多个 Channel,实现多路复用

适用场景:连接数多、连接时间短、高并发的网络应用

架构对比

特性传统 IONIO
模型面向流面向缓冲区
阻塞阻塞式非阻塞式
方向单向(输入/输出分离)双向(Channel 可读写)
选择器Selector 支持多路复用
性能适合少量连接适合大量连接
编程复杂度简单较高

传统 IO 详解

IO 类层次结构

Java IO 按数据类型分为字节流和字符流,按方向分为输入流和输出流:

字节流(处理二进制数据)
├── 输入流 InputStream
│ ├── FileInputStream ─────── 文件输入
│ ├── ByteArrayInputStream ── 内存输入
│ ├── PipedInputStream ────── 管道输入
│ └── FilterInputStream ───── 过滤流(装饰器基类)
│ ├── BufferedInputStream ─ 缓冲输入
│ ├── DataInputStream ───── 基本类型读取
│ └── PushbackInputStream ─ 回退输入

└── 输出流 OutputStream
├── FileOutputStream ────── 文件输出
├── ByteArrayOutputStream ─ 内存输出
├── PipedOutputStream ───── 管道输出
└── FilterOutputStream ──── 过滤流
├── BufferedOutputStream ─ 缓冲输出
├── DataOutputStream ───── 基本类型写入
└── PrintStream ────────── 格式化打印

字符流(处理文本数据)
├── 输入流 Reader
│ ├── FileReader ──────────── 文件字符输入
│ ├── CharArrayReader ─────── 字符数组输入
│ ├── PipedReader ─────────── 管道字符输入
│ ├── BufferedReader ──────── 缓冲字符输入
│ ├── InputStreamReader ───── 字节流转字符流
│ └── StringReader ────────── 字符串输入

└── 输出流 Writer
├── FileWriter ──────────── 文件字符输出
├── CharArrayWriter ─────── 字符数组输出
├── PipedWriter ─────────── 管道字符输出
├── BufferedWriter ──────── 缓冲字符输出
├── OutputStreamWriter ──── 字节流转字符流
└── PrintWriter ─────────── 格式化打印

装饰器模式

Java IO 大量使用了装饰器模式(Decorator Pattern),通过包装基础流来增强功能:

import java.io.*;

// 基础流:文件流
FileInputStream fis = new FileInputStream("data.txt");

// 装饰:添加缓冲功能
BufferedInputStream bis = new BufferedInputStream(fis);

// 再装饰:添加基本类型读取功能
DataInputStream dis = new DataInputStream(bis);

// 读取基本类型
int value = dis.readInt();

为什么使用装饰器模式?

  • 灵活组合:可以根据需要自由组合各种功能
  • 单一职责:每个流类只负责一个功能
  • 开闭原则:新增功能不需要修改现有代码

字节流详解

FileInputStream / FileOutputStream

用于读写二进制文件(图片、视频、音频等):

import java.io.*;

// 写入文件
public static void writeFile(String filename) throws IOException {
// try-with-resources 自动关闭资源
try (FileOutputStream fos = new FileOutputStream(filename)) {
// 写入单个字节
fos.write(65); // 写入 'A'

// 写入字节数组
byte[] data = "Hello, 世界!".getBytes("UTF-8");
fos.write(data);

// 写入部分字节
fos.write(data, 0, 5); // 写入 "Hello"
}
}

// 读取文件
public static void readFile(String filename) throws IOException {
try (FileInputStream fis = new FileInputStream(filename)) {
// 方式一:逐字节读取(效率低,不推荐)
int b;
while ((b = fis.read()) != -1) {
System.out.print((char) b);
}

// 方式二:读取到缓冲区(推荐)
byte[] buffer = new byte[8192]; // 8KB 缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 只处理实际读取的字节
System.out.write(buffer, 0, bytesRead);
}

// 方式三:读取全部(Java 9+,适合小文件)
// byte[] allBytes = fis.readAllBytes();
}
}

关于缓冲区大小的选择

  • 太小(如 1 字节):系统调用频繁,效率低
  • 太大(如 1MB):占用内存多,可能浪费
  • 推荐:4KB 到 64KB 之间,8KB 是常用值

BufferedInputStream / BufferedOutputStream

使用缓冲区减少实际的 I/O 操作次数:

import java.io.*;

// 使用缓冲流复制文件
public static void copyFile(String src, String dest) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream(src));
OutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {

byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
// 不需要手动 flush(),try-with-resources 会自动调用 close()
// close() 内部会调用 flush()
}
}

缓冲流的工作原理

无缓冲:
程序 ──write()──▶ 磁盘(每次 write 都触发系统调用)

有缓冲:
程序 ──write()──▶ 缓冲区(8KB)──满──▶ 磁盘(减少系统调用次数)

DataInputStream / DataOutputStream

读写基本数据类型,保证跨平台一致性:

import java.io.*;

public static void writeData(String filename) throws IOException {
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(filename)))) {

dos.writeInt(123456);
dos.writeDouble(3.14159);
dos.writeBoolean(true);
dos.writeUTF("你好,世界"); // 使用 UTF-8 修改版编码
dos.writeLong(System.currentTimeMillis());
}
}

public static void readData(String filename) throws IOException {
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream(filename)))) {

// 必须按写入顺序读取!
int i = dis.readInt(); // 123456
double d = dis.readDouble(); // 3.14159
boolean b = dis.readBoolean(); // true
String s = dis.readUTF(); // "你好,世界"
long time = dis.readLong(); // 时间戳

System.out.printf("int=%d, double=%f, bool=%b, str=%s%n", i, d, b, s);
}
}

注意事项

  • writeUTF() 使用的是 Java 修改版的 UTF-8 编码,与标准 UTF-8 略有不同
  • 读取顺序必须与写入顺序一致
  • 适合存储结构化的二进制数据

字符流详解

为什么需要字符流?

字节流直接操作字节,但文本文件涉及编码问题:

// 错误示例:用字节流读取 UTF-8 文本可能出错
try (FileInputStream fis = new FileInputStream("text.txt")) {
byte[] bytes = fis.readAllBytes();
// 如果文本包含中文,直接转字符串可能乱码
String content = new String(bytes); // 使用默认编码(可能错误)
}

// 正确方式:指定编码
String content = new String(bytes, "UTF-8");

// 更好的方式:使用字符流
try (FileReader reader = new FileReader("text.txt", StandardCharsets.UTF_8)) {
// 字符流自动处理编码转换
}

InputStreamReader / OutputStreamWriter

字节流与字符流的桥梁,可以指定编码:

import java.io.*;
import java.nio.charset.StandardCharsets;

// 写入时指定编码
public static void writeWithEncoding(String filename, String content) throws IOException {
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream(filename), StandardCharsets.UTF_8)) {
osw.write(content);
}
}

// 读取时指定编码
public static String readWithEncoding(String filename) throws IOException {
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream(filename), StandardCharsets.UTF_8)) {

char[] buffer = new char[1024];
StringBuilder sb = new StringBuilder();
int charsRead;
while ((charsRead = isr.read(buffer)) != -1) {
sb.append(buffer, 0, charsRead);
}
return sb.toString();
}
}

BufferedReader / BufferedWriter

提供缓冲和按行操作:

import java.io.*;
import java.nio.charset.StandardCharsets;

// 读取文本文件
public static void readTextFile(String filename) throws IOException {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(filename), StandardCharsets.UTF_8))) {

// 方式一:逐行读取
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}

// 方式二:Java 8+ Stream API
// br.lines().forEach(System.out::println);
}
}

// 写入文本文件
public static void writeTextFile(String filename, String[] lines) throws IOException {
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(filename), StandardCharsets.UTF_8))) {

for (String line : lines) {
bw.write(line);
bw.newLine(); // 跨平台的换行符
}
}
}

PrintWriter

提供格式化输出功能,不会抛出 IOException:

import java.io.*;
import java.nio.charset.StandardCharsets;

public static void printDemo(String filename) throws IOException {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(new FileOutputStream(filename), StandardCharsets.UTF_8))) {

// println 自动换行
pw.println("Hello, World!");

// 格式化输出
pw.printf("姓名: %s, 年龄: %d%n", "张三", 25);
pw.format("价格: %.2f 元%n", 123.456);

// 检查错误(不抛异常)
if (pw.checkError()) {
System.err.println("写入出错");
}
}
}

标准输入输出

import java.io.*;
import java.util.Scanner;

public class StandardIODemo {
public static void main(String[] args) throws IOException {
// 标准输出
System.out.println("标准输出");
System.err.println("标准错误");

// 标准输入方式一:BufferedReader
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
System.out.print("请输入姓名: ");
String name = br.readLine();
System.out.println("你好," + name);
}

// 标准输入方式二:Scanner(推荐)
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("请输入年龄: ");
if (scanner.hasNextInt()) {
int age = scanner.nextInt();
System.out.println("年龄: " + age);
}
}

// 重定向标准流
System.setIn(new FileInputStream("input.txt"));
System.setOut(new PrintStream(new FileOutputStream("output.txt")));
System.setErr(new PrintStream(new FileOutputStream("error.txt")));
}
}

对象序列化

将 Java 对象转换为字节流,便于存储或传输:

import java.io.*;

// 可序列化的类
public class Person implements Serializable {
// 序列化版本号,用于版本兼容
private static final long serialVersionUID = 1L;

private String name;
private int age;
private transient String password; // transient 字段不参与序列化

public Person(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}

@Override
public String toString() {
return "Person{name=" + name + ", age=" + age + ", password=" + password + "}";
}
}

// 序列化对象
public static void serialize(Object obj, String filename) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(filename)))) {
oos.writeObject(obj);
}
}

// 反序列化对象
@SuppressWarnings("unchecked")
public static <T> T deserialize(String filename) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(filename)))) {
return (T) ois.readObject();
}
}

// 使用
Person person = new Person("张三", 25, "123456");
serialize(person, "person.ser");

Person restored = deserialize("person.ser");
System.out.println(restored); // Person{name=张三, age=25, password=null}

序列化注意事项

  1. 实现 Serializable 接口:标记接口,无方法需要实现
  2. serialVersionUID:建议显式定义,否则编译器自动生成,修改类后可能不兼容
  3. transient:标记不需要序列化的字段(如密码、缓存)
  4. 引用类型:被引用的对象也必须可序列化
  5. 静态字段:不参与序列化,属于类而非对象

NIO 核心组件

NIO 的三大核心组件:Buffer(缓冲区)Channel(通道)Selector(选择器)

Buffer 缓冲区

Buffer 是一个容器,用于存储基本数据类型。它是 NIO 数据操作的基础。

Buffer 的核心属性

根据 Oracle 官方文档,Buffer 有四个核心属性:

0 <= mark <= position <= limit <= capacity
属性说明
capacity容量,缓冲区的最大数据量,创建后不可变
limit限制,第一个不应读/写的元素索引
position位置,下一个要读/写的元素索引
mark标记,调用 reset() 时 position 会回到此位置

Buffer 的状态变化

理解 Buffer 的状态变化是正确使用 NIO 的关键:

1. 新创建的 Buffer(容量为 8)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘

position = 0
limit = 8
capacity = 8

2. 写入 5 个字节后
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘

position = 5
limit = 8
capacity = 8

3. 调用 flip() 切换到读模式
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
▲ ▲
position = 0 limit = 5
capacity = 8

4. 读取 3 个字节后
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
▲ ▲
position = 3 limit = 5
capacity = 8

5. 调用 clear() 重置(数据实际未清除)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘

position = 0
limit = 8
capacity = 8

Buffer 的类型

每种基本类型都有对应的 Buffer:

Buffer 类型说明
ByteBuffer字节缓冲区,最常用
CharBuffer字符缓冲区
ShortBuffer短整型缓冲区
IntBuffer整型缓冲区
LongBuffer长整型缓冲区
FloatBuffer浮点缓冲区
DoubleBuffer双精度缓冲区
MappedByteBuffer文件映射缓冲区

ByteBuffer 详解

ByteBuffer 是最常用的 Buffer,有两种创建方式:

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

// 方式一:堆内存(Heap Buffer)
// 优点:JVM 堆内,GC 管理
// 缺点:IO 操作需要复制到堆外内存
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

// 方式二:直接内存(Direct Buffer)
// 优点:IO 操作效率高,减少复制
// 缺点:分配和释放成本高,不受 GC 直接管理
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

// 写入数据
heapBuffer.put((byte) 65); // 写入单个字节
heapBuffer.put(new byte[]{66, 67, 68}); // 写入字节数组
heapBuffer.put("Hello".getBytes(StandardCharsets.UTF_8)); // 写入字符串

// 切换到读模式(必须!)
heapBuffer.flip();

// 读取数据
byte b = heapBuffer.get(); // 读取单个字节
byte[] arr = new byte[3];
heapBuffer.get(arr); // 读取到数组

// 常用方法
int capacity = heapBuffer.capacity(); // 容量
int position = heapBuffer.position(); // 当前位置
int limit = heapBuffer.limit(); // 限制
int remaining = heapBuffer.remaining(); // 剩余可读元素
boolean hasRemaining = heapBuffer.hasRemaining(); // 是否有剩余

// 标记和重置
heapBuffer.mark(); // 标记当前位置
heapBuffer.get(); // 移动位置
heapBuffer.reset(); // 回到标记位置

// 重绕(不改变 limit,position 归零)
heapBuffer.rewind();

// 清空(position 归零,limit 设为 capacity,数据未清除)
heapBuffer.clear();

// 压缩(丢弃已读数据,未读数据移到前面)
heapBuffer.compact();

Buffer 的相对操作与绝对操作

ByteBuffer buffer = ByteBuffer.allocate(10);

// 相对操作:从当前位置开始,完成后 position 递增
buffer.put((byte) 65); // 相对 put
byte b = buffer.get(); // 相对 get

// 绝对操作:指定索引,不影响 position
buffer.put(0, (byte) 66); // 绝对 put,写入索引 0
byte b2 = buffer.get(1); // 绝对 get,读取索引 1

Channel 通道

Channel 是双向的,可以同时进行读写操作。与 Stream 的单向性不同,Channel 更加灵活。

Channel 类型

Channel 类型说明
FileChannel文件通道,用于文件读写
SocketChannelSocket 客户端通道
ServerSocketChannelSocket 服务端通道
DatagramChannelUDP 通道
Pipe.SinkChannel管道写入端
Pipe.SourceChannel管道读取端

FileChannel 详解

FileChannel 用于文件操作,支持随机访问:

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class FileChannelDemo {

// 读取文件
public static void readFile(String filename) throws Exception {
try (FileChannel channel = FileChannel.open(
Paths.get(filename), StandardOpenOption.READ)) {

ByteBuffer buffer = ByteBuffer.allocate(1024);

// read() 返回读取的字节数,-1 表示到达文件末尾
while (channel.read(buffer) != -1) {
buffer.flip(); // 切换到读模式

while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}

buffer.clear(); // 清空,准备下次读取
}
}
}

// 写入文件
public static void writeFile(String filename, String content) throws Exception {
try (FileChannel channel = FileChannel.open(
Paths.get(filename),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {

ByteBuffer buffer = ByteBuffer.wrap(content.getBytes());
channel.write(buffer);
}
}

// 随机访问(可读可写)
public static void randomAccess(String filename) throws Exception {
try (RandomAccessFile file = new RandomAccessFile(filename, "rw");
FileChannel channel = file.getChannel()) {

// 移动到文件末尾
channel.position(channel.size());

// 写入数据
ByteBuffer buffer = ByteBuffer.wrap("追加内容".getBytes());
channel.write(buffer);

// 移动到文件开头
channel.position(0);

// 读取数据
buffer = ByteBuffer.allocate(1024);
channel.read(buffer);

// 获取文件大小
long size = channel.size();

// 截断文件
channel.truncate(1024);
}
}

// 文件复制(零拷贝,效率最高)
public static void copyFile(String src, String dest) throws Exception {
try (FileChannel srcChannel = FileChannel.open(Paths.get(src), StandardOpenOption.READ);
FileChannel destChannel = FileChannel.open(
Paths.get(dest),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {

// transferTo:从当前通道传输到目标通道
srcChannel.transferTo(0, srcChannel.size(), destChannel);

// 或者使用 transferFrom
// destChannel.transferFrom(srcChannel, 0, srcChannel.size());
}
}

// 内存映射文件(适合大文件)
public static void memoryMappedFile(String filename) throws Exception {
try (FileChannel channel = FileChannel.open(
Paths.get(filename),
StandardOpenOption.READ,
StandardOpenOption.WRITE)) {

// 将文件映射到内存
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_WRITE, // 读写模式
0, // 起始位置
channel.size() // 映射大小
);

// 修改内存中的数据会自动同步到文件
mappedBuffer.put(0, (byte) 'X');

// 强制将修改写入磁盘
mappedBuffer.force();
}
}
}

FileChannel 与传统 IO 的性能对比

操作方式适用场景特点
FileInputStream + BufferedInputStream小文件、简单读取简单易用
FileChannel + ByteBuffer大文件、需要随机访问高效、灵活
transferTo/transferFrom文件复制零拷贝,效率最高
MappedByteBuffer超大文件、频繁修改内存映射,减少复制

Selector 选择器

Selector 是 Java NIO 实现多路复用的核心组件,允许单线程管理多个 Channel。

为什么需要 Selector?

在传统阻塞 IO 模型中,每个连接需要一个线程:

传统模型:
连接1 ──▶ 线程1 ──▶ 阻塞等待数据
连接2 ──▶ 线程2 ──▶ 阻塞等待数据
连接3 ──▶ 线程3 ──▶ 阻塞等待数据
...(1000 个连接 = 1000 个线程)

问题:

  • 线程是昂贵资源(每个线程约 1MB 栈空间)
  • 线程上下文切换开销大
  • 大量空闲线程浪费资源

使用 Selector 的多路复用模型:

NIO 模型:
连接1 ──┐
连接2 ──┼──▶ Selector ──▶ 单个线程处理就绪的 Channel
连接3 ──┤
... ──┘
(1000 个连接 = 1 个线程)

SelectionKey 事件类型

Selector 监听四种事件:

事件常量说明
连接就绪SelectionKey.OP_CONNECT客户端连接成功
接受就绪SelectionKey.OP_ACCEPT服务端收到新连接
读就绪SelectionKey.OP_READ通道有数据可读
写就绪SelectionKey.OP_WRITE通道可以写入数据

Selector 使用示例

下面是一个完整的 NIO 服务器示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

public class NioServer {

public static void main(String[] args) throws IOException {
// 1. 创建 Selector
Selector selector = Selector.open();

// 2. 创建 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 必须设置为非阻塞模式

// 3. 将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("服务器启动,监听端口 8080...");

// 4. 事件循环
while (true) {
// 阻塞等待至少一个 Channel 就绪
int readyChannels = selector.select();

if (readyChannels == 0) {
continue;
}

// 获取就绪的 SelectionKey 集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();

// 从集合中移除,避免重复处理
keyIterator.remove();

if (!key.isValid()) {
continue;
}

// 处理 ACCEPT 事件(新连接)
if (key.isAcceptable()) {
handleAccept(selector, key);
}

// 处理 READ 事件(有数据可读)
if (key.isReadable()) {
handleRead(key);
}
}
}
}

// 处理新连接
private static void handleAccept(Selector selector, SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();

// 接受新连接
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);

// 将客户端 Channel 注册到 Selector,监听 READ 事件
// 可以附加一个 Buffer 或其他对象
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));

System.out.println("新连接: " + clientChannel.getRemoteAddress());
}

// 处理读事件
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();

buffer.clear();

int bytesRead = clientChannel.read(buffer);

if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("连接关闭: " + clientChannel.getRemoteAddress());
clientChannel.close();
key.cancel();
return;
}

if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data, StandardCharsets.UTF_8);

System.out.println("收到消息: " + message);

// 回显消息
buffer.flip();
clientChannel.write(buffer);
}
}
}

NIO 客户端示例

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

public class NioClient {

public static void main(String[] args) throws IOException {
// 创建 SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 非阻塞模式

// 发起连接
boolean connected = socketChannel.connect(new InetSocketAddress("localhost", 8080));

// 如果连接未立即建立,需要等待
if (!connected) {
while (!socketChannel.finishConnect()) {
System.out.println("连接中...");
}
}

System.out.println("连接成功");

// 发送消息
String message = "Hello, NIO Server!";
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));
socketChannel.write(buffer);

// 读取响应
buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);

if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String response = new String(data, StandardCharsets.UTF_8);
System.out.println("服务器响应: " + response);
}

socketChannel.close();
}
}

Selector 的三种 select 方法

Selector selector = Selector.open();

// 1. select() - 阻塞直到至少一个 Channel 就绪
int readyCount = selector.select();

// 2. select(long timeout) - 阻塞最多 timeout 毫秒
int readyCount = selector.select(1000); // 最多阻塞 1 秒

// 3. selectNow() - 非阻塞,立即返回
int readyCount = selector.selectNow();

// wakeup() - 唤醒正在阻塞的 select() 调用
selector.wakeup();

NIO.2 文件操作(Java 7+)

Java 7 引入了 NIO.2(JSR 203),提供了更强大的文件操作 API。

Path 接口

Path 是 NIO.2 中表示文件路径的核心接口:

import java.nio.file.*;

// 创建 Path
Path path1 = Path.of("test.txt"); // Java 11+
Path path2 = Paths.get("test.txt"); // Java 7+
Path path3 = FileSystems.getDefault().getPath("test.txt");

Path absPath = Path.of("/home/user/docs/file.txt");
Path relPath = Path.of("docs/file.txt");

// Path 信息
System.out.println("文件名: " + path1.getFileName());
System.out.println("父路径: " + path1.getParent());
System.out.println("根路径: " + path1.getRoot());
System.out.println("绝对路径: " + path1.toAbsolutePath());
System.out.println("规范化: " + Path.of("/home/user/../docs/./file.txt").normalize());

// 路径组合
Path combined = Path.of("/home", "user", "docs"); // /home/user/docs
Path resolved = path1.resolve("subdir/file.txt"); // 拼接路径
Path sibling = path1.resolveSibling("other.txt"); // 替换文件名

// 路径比较
Path p1 = Path.of("/home/user");
Path p2 = Path.of("/home/user");
System.out.println(p1.equals(p2)); // true
System.out.println(p1.startsWith(p2)); // true

Files 工具类

Files 提供了丰富的文件操作方法:

import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.List;

public class FilesDemo {

public static void main(String[] args) throws Exception {
Path file = Path.of("test.txt");

// === 创建 ===
Files.createFile(file);
Files.createDirectory(Path.of("mydir"));
Files.createDirectories(Path.of("a/b/c/d")); // 创建多级目录
Files.createTempFile("prefix", ".tmp"); // 临时文件
Files.createTempDirectory("temp"); // 临时目录

// === 写入 ===
Files.write(file, "Hello, World!".getBytes());
Files.writeString(file, "你好,世界!"); // Java 11+
Files.write(file, List.of("Line1", "Line2")); // 写入行列表

// 追加内容
Files.write(file, "追加内容".getBytes(), StandardOpenOption.APPEND);

// === 读取 ===
byte[] bytes = Files.readAllBytes(file);
String content = Files.readString(file); // Java 11+
List<String> lines = Files.readAllLines(file);

// === 复制、移动、删除 ===
Files.copy(Path.of("src.txt"), Path.of("dest.txt"),
StandardCopyOption.REPLACE_EXISTING);
Files.move(Path.of("old.txt"), Path.of("new.txt"),
StandardCopyOption.REPLACE_EXISTING);
Files.delete(file); // 不存在会抛异常
Files.deleteIfExists(file); // 不存在返回 false

// === 文件信息 ===
System.out.println("大小: " + Files.size(file));
System.out.println("是否存在: " + Files.exists(file));
System.out.println("是否文件: " + Files.isRegularFile(file));
System.out.println("是否目录: " + Files.isDirectory(file));
System.out.println("是否隐藏: " + Files.isHidden(file));
System.out.println("最后修改: " + Files.getLastModifiedTime(file));

// 文件属性
BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
System.out.println("创建时间: " + attrs.creationTime());
System.out.println("最后访问: " + attrs.lastAccessTime());
System.out.println("最后修改: " + attrs.lastModifiedTime());
System.out.println("大小: " + attrs.size());
System.out.println("是否目录: " + attrs.isDirectory());

// === 遍历目录 ===
// 方式一:DirectoryStream
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Path.of("."))) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}

// 带过滤
try (DirectoryStream<Path> stream = Files.newDirectoryStream(
Path.of("."), "*.java")) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}

// 方式二:Files.list(Java 8+)
Files.list(Path.of(".")).forEach(System.out::println);

// 方式三:深度遍历
Files.walk(Path.of(".")).forEach(System.out::println);

// 方式四:查找文件
Files.find(Path.of("."), Integer.MAX_VALUE,
(path, attrs) -> path.toString().endsWith(".java"))
.forEach(System.out::println);

// 方式五:递归遍历(可控制行为)
Files.walkFileTree(Path.of("."), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("文件: " + file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("目录: " + dir);
return FileVisitResult.CONTINUE;
}
});

// === 文件读写流 ===
try (BufferedReader reader = Files.newBufferedReader(file);
BufferedWriter writer = Files.newBufferedWriter(file)) {
// ...
}

// === 文件通道 ===
try (SeekableByteChannel channel = Files.newByteChannel(file,
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
// ...
}
}
}

WatchService 文件监控

监控文件系统的变化:

import java.nio.file.*;
import java.nio.file.WatchEvent.Kind;

public class WatchServiceDemo {

public static void main(String[] args) throws Exception {
WatchService watchService = FileSystems.getDefault().newWatchService();

Path dir = Path.of(".");

// 注册监控事件
dir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE, // 创建
StandardWatchEventKinds.ENTRY_DELETE, // 删除
StandardWatchEventKinds.ENTRY_MODIFY); // 修改

System.out.println("监控目录: " + dir.toAbsolutePath());

while (true) {
WatchKey key = watchService.take(); // 阻塞等待事件

for (WatchEvent<?> event : key.pollEvents()) {
Kind<?> kind = event.kind();
Path filename = (Path) event.context();

System.out.println(kind.name() + ": " + filename);

// 处理事件
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("新建文件: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("删除文件: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("修改文件: " + filename);
}
}

// 重置 WatchKey,继续监听
boolean valid = key.reset();
if (!valid) {
break;
}
}

watchService.close();
}
}

最佳实践

选择正确的 IO 方式

场景推荐方式
小文件读写Files.readAllBytes/Files.writeString
大文件复制FileChannel.transferTo/transferFrom
文本文件处理BufferedReader/BufferedWriter
高并发网络服务NIO + Selector
简单网络客户端传统 IO 或 NIO
随机访问文件FileChannel 或 RandomAccessFile
文件监控WatchService

资源管理

始终使用 try-with-resources 确保资源关闭:

// 推荐
try (InputStream in = new FileInputStream("file.txt");
OutputStream out = new FileOutputStream("copy.txt")) {
// 操作
}

// 不推荐(需要手动关闭)
InputStream in = null;
try {
in = new FileInputStream("file.txt");
// 操作
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// 忽略
}
}
}

异常处理

IO 操作可能抛出多种异常:

try {
// IO 操作
} catch (FileNotFoundException e) {
// 文件不存在
} catch (IOException e) {
// 其他 IO 错误
} catch (SecurityException e) {
// 权限问题
}

编码处理

始终显式指定编码,避免平台依赖:

// 推荐:显式指定编码
InputStreamReader reader = new InputStreamReader(
new FileInputStream("file.txt"), StandardCharsets.UTF_8);

// 不推荐:使用默认编码
// FileReader reader = new FileReader("file.txt");

小结

本章我们学习了:

  1. IO 与 NIO 的区别:面向流 vs 面向缓冲区、阻塞 vs 非阻塞
  2. 传统 IO:字节流、字符流、装饰器模式、对象序列化
  3. NIO 核心:Buffer(缓冲区)、Channel(通道)、Selector(选择器)
  4. Buffer 操作:capacity、limit、position、flip、clear、compact
  5. Channel 使用:FileChannel 文件操作、SocketChannel 网络通信
  6. Selector 多路复用:单线程管理多连接的高效模型
  7. NIO.2 文件 API:Path、Files、WatchService
  8. 最佳实践:资源管理、异常处理、编码处理

练习

  1. 使用 FileChannel 实现大文件的高效复制
  2. 使用 NIO Selector 实现一个简单的聊天服务器
  3. 使用 Files API 递归统计目录下各类文件的数量
  4. 使用 WatchService 实现配置文件热更新
  5. 比较传统 IO 和 NIO 复制大文件的性能差异