NIO 中的 Buffer(缓冲区) 是 Java NIO 的核心组件之一,它是一个用于存储特定基本类型数据的容器。所有 NIO 的 I/O 操作都是通过 Buffer 来进行的------数据总是从 Channel 读入 Buffer,或从 Buffer 写入 Channel。
一、Buffer 的基本类型
Java NIO 提供了 7 种主要的 Buffer 实现,对应 7 种基本数据类型(除了 boolean):
| 类型 | 说明 |
|---|---|
ByteBuffer |
最常用,其他类型可通过它转换 |
CharBuffer |
字符缓冲区 |
ShortBuffer |
短整型缓冲区 |
IntBuffer |
整型缓冲区 |
LongBuffer |
长整型缓冲区 |
FloatBuffer |
浮点缓冲区 |
DoubleBuffer |
双精度缓冲区 |
其中 ByteBuffer 是最核心的,因为网络和文件 I/O 的底层都是字节流。
二、Buffer 的四个核心属性
理解 Buffer 的关键在于掌握它的四个属性:
java
public abstract class Buffer {
private int mark = -1; // 标记位置
private int position = 0; // 当前位置
private int limit; // 限制位置
private int capacity; // 容量
}
1. capacity(容量)
- Buffer 能容纳的最大数据量,创建后不可改变。
2. position(位置)
- 下一个要被读或写的元素的索引。
- 写模式下:写入数据的位置。
- 读模式下:读取数据的位置。
3. limit(限制)
- 缓冲区中可读/可写的边界,不能超过 limit。
- 写模式:limit = capacity。
- 读模式:limit = 之前写入的数据量。
4. mark(标记)
- 一个临时记录的位置,通过
mark()设置,reset()恢复到该位置。
关系恒成立: 0 <= mark <= position <= limit <= capacity
三、核心方法
1. 读写操作
java
// 写数据:position 递增
buffer.put((byte) 1);
// 读数据:position 递增
byte b = buffer.get();
2. flip() ------ 写模式切换为读模式
java
public final Buffer flip() {
limit = position; // limit 设为已写入的数据量
position = 0; // position 归零
mark = -1;
return this;
}
这是最容易出错 的地方:写完数据要读之前,必须调用 flip()。
3. clear() ------ 清空缓冲区
java
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
注意:clear() 并不真正清除数据,只是重置指针,数据会被后续写入覆盖。
4. compact() ------ 保留未读数据
java
public abstract ByteBuffer compact();
将未读的数据复制到缓冲区起始处,然后切换到写模式。用于读取部分数据后继续写入的场景。
5. rewind() ------ 重读
java
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
将 position 归零,可以重新读取已读过的数据,limit 不变。
6. mark() 和 reset()
java
buffer.mark(); // 记录当前 position
buffer.reset(); // 恢复到 mark 的位置
四、状态转换示意图
初始状态(刚创建):
0 capacity
| |
position=0, limit=capacity
写入 3 个字节后:
0 1 2 3 capacity
| | |
position=3, limit=capacity
调用 flip() 后(切换到读模式):
0 1 2 3
| |
position=0, limit=3
读取 2 个字节后:
0 1 2 3
| |
position=2, limit=3
调用 clear() 后(回到写模式):
0 1 2 3 capacity
| |
position=0, limit=capacity
五、使用示例
java
public class BufferDemo {
public static void main(String[] args) {
// 创建容量为 10 的 ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(10);
// 写模式
buffer.put((byte) 'H');
buffer.put((byte) 'e');
buffer.put((byte) 'l');
buffer.put((byte) 'l');
buffer.put((byte) 'o');
// 切换到读模式
buffer.flip();
// 读取数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// 输出: Hello
// 清空,准备下次写入
buffer.clear();
}
}
六、allocate vs allocateDirect
java
// 1. 堆内存缓冲区(JVM 堆中)
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 2. 直接内存缓冲区(操作系统内存,零拷贝)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
| 对比项 | allocate(堆) | allocateDirect(直接) |
|---|---|---|
| 内存位置 | JVM 堆 | 堆外(OS 内存) |
| 分配/回收速度 | 快 | 慢 |
| I/O 性能 | 较低(需复制) | 高(零拷贝) |
| 适用场景 | 普通场景 | 高频 I/O、大文件 |
七、其他常用方法
java
buffer.remaining(); // limit - position,剩余可读/可写数量
buffer.hasRemaining(); // 是否还有剩余
buffer.rewind(); // 重置 position 为 0,可重读
buffer.slice(); // 创建一个共享数据的子缓冲区
buffer.duplicate(); // 复制一个缓冲区(共享数据,独立属性)
buffer.asReadOnlyBuffer();// 创建只读缓冲区
八、常见误区
- 忘记 flip():写完直接读,会读到 position 之后的位置,导致读不到数据。
- 混淆 clear() 和真正清空 :
clear()只重置指针,不擦除内容。 - 读完后直接 clear() 导致未读数据丢失 :应使用
compact()保留未读数据。 - 多次 flip():flip 只能调一次,第二次调用会把 limit 设为 0。
九、总结
Buffer 是 NIO 数据读写的桥梁,核心在于四个属性的流转:
- 写数据 → position 增加
- flip() → 切换到读模式
- 读数据 → position 增加
- clear()/compact() → 切换回写模式
掌握 position、limit、capacity 三者的变化,就掌握了 Buffer 的本质。