Java NIO:compact() 的正确理解与常见问题
一、compact() 是什么?
compact() 的作用是:保留 Buffer 中尚未读取的数据,将它们移动到缓冲区开头,并将 position 设置为剩余数据的长度,以便继续写入新数据。
它主要解决一个问题:
当 Buffer 中的数据没有处理完,又需要接收新数据时,如何保留之前未处理的数据?
例如:
Buffer 中有 5 个字节:
调用:
buffer.flip();
buffer.get(); // 读取10
buffer.get(); // 读取20此时:
capacity = 10
limit = 5
position = 2已经读取了 10、20,剩余未读取的数据:
30、40、50调用:
buffer.compact();执行后:
此时:
capacity = 10
limit = 10
position = 3注意:原来的 10、20 已经不再属于有效数据。原来索引 3、4 的旧数据即使还留在物理内存中,也不能当作有效数据使用。
二、compact() 的执行原理
可以将 compact() 理解为三个操作:
将 position 到 limit 之间尚未读取的数据,移动到 Buffer 开头。
将 position 设置为剩余数据的长度。
将 limit 设置为 capacity。
例如:
执行前:
索引: 0 1 2 3 4
数据: 10 20 30 40 50
↑
position=2
limit=5执行 compact():
执行后:
索引: 0 1 2 3 4
数据: 30 40 50 - -
↑
position=3
limit=10原理上可以理解为:
// 伪代码,仅用于理解原理
int remaining = limit - position;
// 将未读取的数据移动到开头
moveRemainingDataToBeginning();
// 更新状态
position = remaining;
limit = capacity;实际 JDK 实现会根据 Buffer 类型和底层内存操作完成数据移动。
三、compact() 最容易踩的坑
1. compact() 后直接调用 rewind(),可能读取到无效数据
这是最容易混淆的地方。
假设 compact() 执行后:
有效数据:30、40、50
position = 3
limit = 10
capacity = 10此时调用:
buffer.rewind();结果:
position = 0
limit = 10
capacity = 10再调用:
buffer.get();确实会读取到 30。
但是问题在于:
rewind() 只会把 position 设置为 0,不会修改 limit。
因此后续还允许读取索引 3~9,而这些位置可能存在旧数据或无效数据。
正确理解:compact() 后,如果想重新读取当前保留的数据,需要重新设置读取边界。
例如:
buffer.compact();
// 切换为读取模式
buffer.flip();此时:
position = 0
limit = 3
capacity = 10只能读取:
30、40、50注意:如果 compact() 后还需要继续接收新数据,就不要立即 flip(),而应该直接进行后续写入,等写入完成后再 flip()。
2. compact() 和 clear() 混用,导致未处理数据丢失
假设 Buffer 中有:
10、20、30、40、50已经读取了:
10、20剩余:
30、40、50如果此时调用:
buffer.clear();Buffer 会变成:
position = 0
limit = capacity之前未读取的 30、40、50 虽然可能还在内存中,但已经失去了有效数据范围,后续写入还可能覆盖它们。
如果业务要求保留未读取的数据,就应该使用 compact(),而不是 clear()。
两者区别:
3. compact() 后 position 不是 0
很多人容易误以为 compact() 后 position 会归零。
实际上:
position = 剩余数据的长度例如剩余 3 个字节:
position = 3
limit = 10为什么?
因为索引 0~2 已经存放了之前未读取的数据,下一次写入应该从索引 3 开始,不能覆盖之前的数据。
例如:
buffer.compact();
buffer.put((byte) 60);60 会被写入索引 3。
此时有效数据:
30、40、50、604. compact() 后直接读取新数据
compact() 后 Buffer 处于写入状态。
例如:
position = 3
limit = 10此时如果直接调用:
buffer.get();读取的将是索引 3 的数据,而不是之前保留数据的第一个字节。
因为 position 当前指向的是下一次写入的位置。
如果想读取整个有效数据,应该先调用 flip():
buffer.flip();然后再读取。
四、compact() 在网络编程中的实际应用
compact() 在 NIO 网络编程中非常重要。
因为 TCP 是字节流协议,不保证一次 read() 就能读取完整的业务数据。
例如,服务端规定每条消息长度为 5 个字节。
客户端发送:
10、20、30、40、50但服务端第一次只读取到了:
10、20、30这时数据还不完整,需要等待后续数据。
假设业务已经处理完前面有效的部分,但还有部分数据需要保留,那么可以通过 compact() 保留未处理的数据,继续接收后续数据。
典型流程:
// 1. 将 Buffer 切换为读取模式
buffer.flip();
// 2. 处理已经完整的数据
while (buffer.hasRemaining()) {
// 根据协议判断数据是否完整
// 完整则处理
// 不完整则停止读取,保留剩余数据
}
// 3. 保留未处理的数据
buffer.compact();
// 4. 继续从 Channel 接收新数据
channel.read(buffer);
// 5. 再次切换为读取模式
buffer.flip();注意:上面是流程示意,实际网络编程需要先判断消息是否完整,不能直接将所有剩余数据都当作完整消息处理。channel.read(buffer) 也可能返回 0 或 -1,需要分别处理无数据和连接关闭的情况。
五、正确使用 compact() 的完整示例
import java.nio.ByteBuffer;
public class CompactDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
// 1. 写入原始数据
buffer.put((byte) 10);
buffer.put((byte) 20);
buffer.put((byte) 30);
buffer.put((byte) 40);
buffer.put((byte) 50);
// 2. 切换读取模式
buffer.flip();
// 3. 读取前两个字节
System.out.println(buffer.get()); // 10
System.out.println(buffer.get()); // 20
// 4. 保留未读取的数据
buffer.compact();
// 此时 position=3,limit=10
// 5. 继续写入新数据
buffer.put((byte) 60);
// 6. 再次切换读取模式
buffer.flip();
// 7. 读取剩余数据
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
}
}输出:
10
20
30
40
50
60其中 compact() 保留了 30、40、50,后续又写入了 60。
六、重点总结
compact() 会保留 position 到 limit 之间未读取的数据。
未读取的数据会被移动到 Buffer 开头。
compact() 后 position 等于剩余数据的长度。
compact() 后 limit 等于 capacity,Buffer 进入可写状态。
已经读取的数据不再属于当前有效数据范围。
compact() 不会清空整个内存,旧数据可能仍然存在。
compact() 后不能随意使用 rewind(),否则可能读取到无效数据。
如果要重新读取 compact() 保留的数据,可以调用 flip()。
如果还要接收新数据,则应该先继续写入,写入完成后再 flip()。
网络编程中,compact() 常用于保留未处理完的数据,等待后续 TCP 数据到达。
核心记忆:
compact() 不是清空 Buffer,而是把还没处理完的数据移动到开头,腾出后面的空间继续接收新数据。
七、常见问题与答案
1. compact() 会覆盖已经读取的数据吗?
从逻辑上看,已经读取的数据被丢弃,未读取的数据移动到开头。原来的数据可能还存在于内存,但不再属于有效数据。
2. compact() 后为什么 position 不为 0?
因为开头已经存放了未读取的数据,position 必须指向剩余数据后面的位置,以便继续写入。
3. compact() 和 clear() 最大的区别是什么?
clear() 不保留未读取的数据,compact() 会保留未读取的数据。
4. compact() 后为什么不能直接 rewind()?
因为 rewind() 不会改变 limit,而 compact() 已经将 limit 设置为 capacity,可能导致读取到无效数据。
5. compact() 后什么时候调用 flip()?
如果需要重新读取保留的数据,可以立即 flip();如果还需要接收新数据,则先写入新数据,写入完成后再 flip()。
