Java NIO 缓冲区高级特性(二)

上一篇介绍了缓冲区的基本属性,本文将详细讲解缓冲区的压缩、标记等高级特性,以及现代Java NIO缓冲区的最佳实践。

1. 缓冲区压缩(Compact)

1.1 compact() 方法

compact() 方法是 ByteBuffer 中的一个重要方法,用于处理部分读取后的数据:

1
2
3
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
public abstract ByteBuffer compact();
}

1.2 压缩原理

当从缓冲区中读取部分数据后,compact() 方法会:

  1. 将未读取的数据(从 position 到 limit 之间的数据)移动到缓冲区的起始位置
  2. 将 position 设置为未读取数据的长度
  3. 将 limit 设置为 capacity
  4. 清除 mark 标记

1.3 压缩示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.nio.ByteBuffer;

public class BufferCompactExample {
public static void main(String[] args) {
// 创建一个容量为 10 的缓冲区
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.put((byte) ' ');
buffer.put((byte) 'W');
buffer.put((byte) 'o');
buffer.put((byte) 'r');
buffer.put((byte) 'l');

// 切换到读取模式
buffer.flip();

// 读取前 5 个字节
for (int i = 0; i < 5; i++) {
System.out.print((char) buffer.get());
}
System.out.println(); // 输出: Hello

// 压缩缓冲区
buffer.compact();

// 现在可以写入新数据
buffer.put((byte) 'd');
buffer.put((byte) '!');

// 切换到读取模式
buffer.flip();

// 读取所有数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println(); // 输出: World!
}
}

1.4 压缩的应用场景

  • 数据处理管道:当需要处理部分数据后继续添加新数据时
  • 网络通信:处理不完整的网络数据包
  • 文件 I/O:处理大文件时的分块读写

2. 缓冲区标记(Mark)

2.1 标记相关方法

  • mark():设置当前 position 为标记位置
  • reset():将 position 恢复到标记位置
  • clear():清除标记

2.2 标记示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.nio.ByteBuffer;

public class BufferMarkExample {
public static void main(String[] args) {
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();

// 读取两个字节
System.out.print((char) buffer.get()); // H
System.out.print((char) buffer.get()); // e

// 设置标记
buffer.mark();

// 继续读取两个字节
System.out.print((char) buffer.get()); // l
System.out.print((char) buffer.get()); // l

// 重置到标记位置
buffer.reset();

// 再次读取
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get()); // lo
}
System.out.println(); // 输出: Hello
}
}

2.3 标记的注意事项

  • 标记是可选的,不是所有操作都需要标记
  • 调用 reset() 前必须先调用 mark(),否则会抛出 InvalidMarkException
  • clear()compact()flip() 等操作会清除标记

3. 缓冲区比较与排序

3.1 比较方法

1
public abstract int compareTo(ByteBuffer that);

比较规则:

  1. 比较剩余字节的数量
  2. 比较第一个不同的字节
  3. 如果一个缓冲区是另一个的前缀,则较短的缓冲区被认为较小

3.2 比较示例

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.nio.ByteBuffer;

public class BufferCompareExample {
public static void main(String[] args) {
ByteBuffer buffer1 = ByteBuffer.wrap("Hello".getBytes());
ByteBuffer buffer2 = ByteBuffer.wrap("Hello".getBytes());
ByteBuffer buffer3 = ByteBuffer.wrap("World".getBytes());

System.out.println("buffer1.compareTo(buffer2): " + buffer1.compareTo(buffer2)); // 0
System.out.println("buffer1.compareTo(buffer3): " + buffer1.compareTo(buffer3)); // 负数
System.out.println("buffer3.compareTo(buffer1): " + buffer3.compareTo(buffer1)); // 正数
}
}

4. 缓冲区视图

4.1 视图类型

  • asCharBuffer():创建字符缓冲区视图
  • asShortBuffer():创建短整型缓冲区视图
  • asIntBuffer():创建整型缓冲区视图
  • asLongBuffer():创建长整型缓冲区视图
  • asFloatBuffer():创建浮点型缓冲区视图
  • asDoubleBuffer():创建双精度浮点型缓冲区视图

4.2 视图示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.nio.ByteBuffer;
import java.nio.IntBuffer;

public class BufferViewExample {
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocate(16);

// 写入四个整数
for (int i = 0; i < 4; i++) {
byteBuffer.putInt(i * 10);
}

// 创建整型缓冲区视图
byteBuffer.flip();
IntBuffer intBuffer = byteBuffer.asIntBuffer();

// 读取数据
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get()); // 0, 10, 20, 30
}
}
}

5. 直接缓冲区与非直接缓冲区

5.1 直接缓冲区

1
2
3
4
5
// 创建直接缓冲区
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

// 检查是否为直接缓冲区
boolean isDirect = directBuffer.isDirect();

5.2 直接缓冲区的优势

  • 性能优势:在 I/O 操作时避免了数据在用户空间和内核空间之间的复制
  • 适用于:频繁的 I/O 操作,如网络通信和文件 I/O

5.3 直接缓冲区的劣势

  • 创建成本高:分配和释放直接缓冲区的成本较高
  • 内存管理:不受 Java 垃圾回收器直接管理

6. 缓冲区最佳实践

6.1 选择合适的缓冲区类型

场景 推荐缓冲区类型 原因
网络通信 直接 ByteBuffer 减少数据复制,提高性能
内存数据处理 非直接 ByteBuffer 创建成本低,适合频繁创建和销毁
字符数据 CharBuffer 专门用于字符处理
数值数据 IntBuffer, LongBuffer 等 类型安全,避免类型转换错误

6.2 性能优化技巧

  1. 重用缓冲区:避免频繁创建和销毁缓冲区
  2. 合理设置容量:根据实际需求设置合适的缓冲区大小
  3. 使用直接缓冲区:对于 I/O 密集型操作
  4. 正确使用 flip():在读写切换时使用
  5. 使用 compact():在部分读取后继续写入时使用

6.3 常见错误及解决方案

错误 原因 解决方案
BufferUnderflowException 读取超过 limit 位置 使用 hasRemaining() 检查
BufferOverflowException 写入超过 capacity 检查剩余空间或扩容
InvalidMarkException 调用 reset() 前未调用 mark() 确保先调用 mark()
ReadOnlyBufferException 尝试写入只读缓冲区 使用可写缓冲区

7. 现代 Java NIO 缓冲区使用示例

7.1 网络通信示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NioServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(8080));

System.out.println("Server started on port 8080");

while (true) {
SocketChannel client = serverSocket.accept();
System.out.println("Client connected: " + client.getRemoteAddress());

// 使用直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

while (client.read(buffer) > 0) {
buffer.flip();

// 处理数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}

buffer.clear();
}

client.close();
}
}
}

7.2 文件 I/O 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class FileIOExample {
public static void main(String[] args) throws IOException {
// 写入文件
try (FileChannel fileChannel = FileChannel.open(
Paths.get("example.txt"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, NIO!".getBytes());
buffer.flip();

fileChannel.write(buffer);
}

// 读取文件
try (FileChannel fileChannel = FileChannel.open(
Paths.get("example.txt"),
StandardOpenOption.READ)) {

ByteBuffer buffer = ByteBuffer.allocate(1024);

while (fileChannel.read(buffer) > 0) {
buffer.flip();

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

buffer.clear();
}
}
}
}

8. 缓冲区与现代 Java 特性

8.1 与 Stream API 结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.nio.ByteBuffer;
import java.util.stream.IntStream;

public class BufferStreamExample {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);

// 使用 Stream 填充缓冲区
IntStream.range(0, 10).forEach(i -> buffer.put((byte) (i + 65)));

buffer.flip();

// 使用 Stream 处理缓冲区数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println(); // 输出: ABCDEFGHIJ
}
}

8.2 与 CompletableFuture 结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;

public class BufferAsyncExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < 10; i++) {
buffer.put((byte) (i + 65));
}
return buffer;
}).thenAccept(buffer -> {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
}).join();
}
}

9. 参考资料