ByteBuffer 的 position、limit、capacity 是什么?flip、clear、compact 有什么区别?
简化版
capacity 是缓冲区总容量,position 是下一个要读或写的位置,limit 是当前模式下不能越过的边界。flip() 把已写入的数据切换为可读;clear() 丢弃未读状态,准备重写整个 Buffer;compact() 先把未读数据移到开头,再从其后继续写。
详细版
Buffer 始终满足:
0 <= position <= limit <= capacity
capacity:创建后固定的容量。position:相对get()/put()下一次操作的索引,操作后会向后移动。limit:第一个不允许被当前读写操作访问的索引。mark:可选的位置书签,reset()可将position回到 mark。
典型流程是:创建 Buffer 后处于写模式;向其中写数据;调用 flip();从位置 0 读到原写入位置;读完后调用 clear() 或未读完时调用 compact()。
完整版教学
一、用一个 8 字节 Buffer 理解状态
ByteBuffer buffer = ByteBuffer.allocate(8);
刚创建时:
position = 0, limit = 8, capacity = 8
写入 3 个字节后:
position = 3, limit = 8, capacity = 8
这时如果直接调用相对 get(),它会从位置 3 开始读,而不是读刚写入的前 3 个字节。flip() 完成两件事:
limit = position; // limit = 3
position = 0;
于是可读区间变成 [0, 3)。这就是“翻转”的含义:不是翻转数据,而是改变边界,把当前 Buffer 从填充模式转成消费模式。
二、clear 不会把字节清零
clear() 的结果是:
position = 0
limit = capacity
mark = undefined
它只重置状态指针,原来的字节可能仍在内存中,只是之后的 put() 可以覆盖它们。因此 clear() 不是安全擦除,也不应用来保证敏感数据从内存中消失。
三、compact 为什么用于半包和部分写
假设读模式下 position = 3、limit = 8,说明 [3, 8) 还没消费。compact() 会把这 5 个字节复制到 Buffer 开头,然后设置:
position = 5
limit = capacity
程序可以继续从位置 5 写入新数据,原未处理数据不会丢失。网络协议解码时,一次 read() 可能只得到半条消息,这时就应保留已到达的部分,等下一批字节拼接。
int count;
while ((count = channel.read(buffer)) > 0) {
buffer.flip();
decodeCompleteFrames(buffer); // position 停在未解析数据的开头
buffer.compact(); // 保留未解析数据
}
四、rewind、mark 和绝对访问
rewind():把position设为 0,保留limit,适合重新读一遍当前数据。mark()/reset():保存并回到一个位置。当 position 或 limit 被调整到 mark 之前,mark 会失效。get(index)/put(index, value):绝对访问不改变position,适合查看协议头或回填长度字段。remaining()等于limit - position,hasRemaining()用来判断当前还有没有可读或可写空间。
五、一个常见的部分写模式
buffer.flip();
while (buffer.hasRemaining()) {
int written = channel.write(buffer);
if (written == 0) {
// 非阻塞 Channel 暂时不可写,保留 Buffer 状态等 OP_WRITE
break;
}
}
if (buffer.hasRemaining()) {
buffer.compact();
} else {
buffer.clear();
}
真正的非阻塞服务器通常会把待发 Buffer 保存在连接上下文中,等 OP_WRITE 就绪后继续写。绝不能在未写完时就 clear(),否则剩余数据会被新数据覆盖。
六、视图、切片与字节序
duplicate() 创建共享同一数据但拥有独立 position、limit、mark 的视图;slice() 则从当前 position 到 limit 创建子视图,子视图的 position 从 0 开始。它们通常不复制底层字节,因此修改一个可写视图会影响其他共享视图,不能把它们当作数据快照。
多字节整数还受 ByteOrder 影响。网络协议常用大端序,ByteBuffer 默认也是 BIG_ENDIAN;如果协议规定小端序,就必须显式调用 order(ByteOrder.LITTLE_ENDIAN)。例如字节 01 02 03 04 按大端读取为 0x01020304,按小端读取则为 0x04030201。
| 操作 | 数据是否复制 | 新视图初始范围 | 是否共享修改 |
|---|---|---|---|
duplicate() | 否 | 原 Buffer 的完整状态范围 | 是 |
slice() | 否 | 原 position 到 limit | 是 |
| 手动复制到新 Buffer | 是 | 由新 Buffer 决定 | 否 |
易错点:Buffer 的“状态独立”不等于“数据独立”;duplicate 和 slice 的游标各走各的,但底层内容仍可能是同一块内存。
七、常见误区与追问
- 误区:
flip()会把 Buffer 中的字节倒序。 它只把旧 position 设为新 limit,再把 position 归零。 - 误区:
clear()会将旧数据全部置零。 它只重置边界,旧字节仍可能留在底层存储中并等待覆盖。 - 误区:一次 Channel
read()就能填满 Buffer。 返回值取决于可用数据、EOF 和通道模式,可能小于 remaining,非阻塞时还可能为 0。 - 误区:一次 Channel
write()一定写完整个 Buffer。 写操作可能只推进部分 position,必须检查hasRemaining()。 - 追问:为什么半包解析后用
compact()而不是clear()? compact 会把[position, limit)的未消费字节搬到开头,为下一批数据腾出连续空间。 - 追问:绝对
get(index)会推进 position 吗? 不会,它适合查看协议头;相对 get 才会推进游标。 - 追问:
slice()是不是线程安全副本? 不是,它共享内容,且 ByteBuffer 本身通常不保证并发访问安全,所有权仍需协调。
八、加强记忆
Buffer 的核心是 position 在 limit 之前移动,capacity 则是固定上限。写完准备读用 flip(),数据全部消费后用 clear(),还有未读数据时用 compact() 保留剩余再继续写。