Java NIO缓冲区详解:核心概念与基础操作
缓冲区(Buffer)概述
缓冲区本质上是一块连续的内存区域,用于存储特定类型的数据。从实现角度来看,Java NIO中的Buffer类内部封装了一个基本数据类型的数组,并提供了操作该数组的各种方法。
常见的缓冲区类型包括ByteBuffer、IntBuffer、DoubleBuffer等,它们分别对应byte、int、double等基本数据类型的数组。
缓冲区与通道的交互
在Java NIO架构中,缓冲区主要与通道(Channel)配合使用。数据的传输总是在缓冲区和通道之间进行:要么将缓冲区中的数据写入通道,要么从通道读取数据到缓冲区中。这种设计模式使得I/O操作更加高效和灵活。
缓冲区类层次结构
以ByteBuffer为例,其继承体系如下:

Buffer是顶层抽象类,ByteBuffer继承自Buffer,也是一个抽象类。ByteBuffer的两个主要实现类是:
- DirectByteBuffer:分配在JVM堆外内存,通过unsafe.allocateMemory实现
- HeapByteBuffer:分配在JVM堆内内存
缓冲区的核心属性
每个缓冲区都有四个重要属性,它们共同决定了缓冲区的状态和行为:
容量(capacity)
容量表示缓冲区能够容纳的最大元素数量,该值在缓冲区创建时确定且不能更改。例如,创建一个容量为10的字节缓冲区:
ByteBuffer buffer = ByteBuffer.allocate(10);
界限(limit)
界限表示缓冲区中第一个不能被读写元素的索引,也可以理解为缓冲区中当前可操作元素的数量。
位置(position)
位置表示下一个将被读写的元素的索引,该值会随着get()和put()方法的调用而自动更新。
标记(mark)
标记是一个备忘位置。调用mark()方法会将当前position的值保存到mark中,之后调用reset()方法可以将position恢复到之前标记的位置。
这四个属性满足以下关系:0 ≤ mark ≤ position ≤ limit ≤ capacity
缓冲区操作示例
让我们通过一个示例来观察缓冲区属性的变化过程:
1. 创建容量为10的字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
此时属性状态:mark = -1, position = 0, limit = 10, capacity = 10

2. 向缓冲区写入5个字节
buffer.put((byte)'J').put((byte)'a').put((byte)'v').put((byte)'a').put((byte)'!');
此时属性状态:mark = -1, position = 5, limit = 10, capacity = 10

3. 调用flip()方法准备读取
buffer.flip();
此时属性状态:mark = -1, position = 0, limit = 5, capacity = 10

4. 读取两个元素
System.out.println("" + (char) buffer.get() + (char) buffer.get());
此时属性状态:mark = -1, position = 2, limit = 5, capacity = 10

5. 标记当前位置
buffer.mark();
此时属性状态:mark = 2, position = 2, limit = 5, capacity = 10
6. 读取两个元素后恢复到标记位置
System.out.println("" + (char) buffer.get() + (char) buffer.get());
buffer.reset();
执行第一行后:mark = 2, position = 4, limit = 5, capacity = 10

执行第二行后:mark = 2, position = 2, limit = 5, capacity = 10

7. 调用compact()方法压缩缓冲区
buffer.compact();
此时属性状态:mark = 2, position = 3, limit = 10, capacity = 10

注意compact()方法会丢弃已读数据,保留未读数据,并将位置移动到未读数据之后,为新的写入操作做准备。
缓冲区比较机制
缓冲区的相等性判断遵循特定规则。以下是ByteBuffer的equals方法实现:
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof ByteBuffer))
return false;
ByteBuffer that = (ByteBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
for (int i = this.limit() - 1, j = that.limit() - 1; i >= p; i--, j--)
if (!equals(this.get(i), that.get(j)))
return false;
return true;
}
两个缓冲区被视为相等的条件是:
- 两个对象类型相同。不同数据类型的缓冲区永远不会相等。
- 两个对象剩余元素数量相同。缓冲区容量可以不同,剩余数据的索引位置也可以不同,但从position到limit之间的元素数量必须相同。
- 两个缓冲区中剩余数据元素序列必须完全一致。
批量数据操作
ByteBuffer提供了多种批量读写数据的方法:
public ByteBuffer get(byte[] dest, int offset, int length)
public ByteBuffer put(byte[] src, int offset, int length)
public ByteBuffer get(byte[] dest)
public final ByteBuffer put(byte[] src)
后两种方法实际上是前两种方法的简化形式。参数含义如下(以put方法为例):
/*
* @param src
* 要读取的字节数组
*
* @param offset
* 数组中第一个要读取的字节的偏移量;
* 必须非负且不大于 array.length
*
* @param length
* 要从给定数组中读取的字节数;
* 必须非负且不大于 array.length - offset
*/
批量操作可以显著提高I/O效率,特别是在处理大量数据时。