3
0
0

Java NIO:compact() 的正确理解与常见问题

文章摘要
|

一、compact() 是什么?

compact() 的作用是:保留 Buffer 中尚未读取的数据,将它们移动到缓冲区开头,并将 position 设置为剩余数据的长度,以便继续写入新数据。

它主要解决一个问题:

当 Buffer 中的数据没有处理完,又需要接收新数据时,如何保留之前未处理的数据?

例如:

Buffer 中有 5 个字节:

索引

0

1

2

3

4

数据

10

20

30

40

50

调用:

buffer.flip();

buffer.get(); // 读取10
buffer.get(); // 读取20

此时:

capacity = 10
limit = 5
position = 2

已经读取了 10、20,剩余未读取的数据:

30、40、50

调用:

buffer.compact();

执行后:

索引

0

1

2

3

4

有效数据

30

40

50

空

空

此时:

capacity = 10
limit = 10
position = 3

注意:原来的 10、20 已经不再属于有效数据。原来索引 3、4 的旧数据即使还留在物理内存中,也不能当作有效数据使用。

二、compact() 的执行原理

可以将 compact() 理解为三个操作:

  1. 将 position 到 limit 之间尚未读取的数据,移动到 Buffer 开头。

  2. 将 position 设置为剩余数据的长度。

  3. 将 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()。

两者区别:

方法

是否保留未读取的数据

用途

clear()

不保留

重新使用整个 Buffer

compact()

保留

保留剩余数据,继续接收新数据

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、60

4. 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。

六、重点总结

  1. compact() 会保留 position 到 limit 之间未读取的数据。

  2. 未读取的数据会被移动到 Buffer 开头。

  3. compact() 后 position 等于剩余数据的长度。

  4. compact() 后 limit 等于 capacity,Buffer 进入可写状态。

  5. 已经读取的数据不再属于当前有效数据范围。

  6. compact() 不会清空整个内存,旧数据可能仍然存在。

  7. compact() 后不能随意使用 rewind(),否则可能读取到无效数据。

  8. 如果要重新读取 compact() 保留的数据,可以调用 flip()。

  9. 如果还要接收新数据,则应该先继续写入,写入完成后再 flip()。

  10. 网络编程中,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()。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论