在上一篇文章中聊了一下Channel,这篇文章聊一下Java NIO中的另一个重要的组件:Buffer。
Buffer的概念
首先我们先看一下Buffer的类图,Buffer在java.nio中被定义为抽象类:

Buffer被称为内存缓冲区,本质上也是内存中的一块,我们可以将数据写入内存中,之后从这块内存中读取数据。也可以将这个块内存封装成NIO Buffer对象,并提供一组常用的方法,方便对这块内存进行读写操作。
在Java NIO中,所有的数据都要经过Buffer,我们可以将Buffer理解为一个数组的封装,我们最常用的ByteBuffer对应的数据结构就是byte[],下图是Buffer内部的基本结构。

再来看一下代码中Buffer的定义。

mark属性:代表标记,通过mark()方法,记录当前position值,将position值赋值给mark,在后续的写入或读取过程中,可以通过reset()方法恢复当前position为mark记录的值position属性:代表读取或者写入Buffer的位置,代表读取或写入的下标,默认为0- 每往Buffer中写入一个值,position就会自动加1,代表下一次写入的位置
- 每往Buffer中读取一个值,position就自动加1,代表下一次读取的位置
capacity属性:Buffer能够容纳的数据元素的最大值,在Buffer初始化创建的时候被赋值,而且不能被修改。limit属性:代表Buffer可读可写的上限。- 写模式下:limit 代表能写入数据的上限位置,这个时候limit = capacity
- 读模式下:在Buffer完成所有数据写入后,通过调用flip()方法,切换到读模式,此时limit等于Buffer中实际已经写入的数据大小。因为Buffer可能没有被写满,所以limit<=capacity
Buffer常用API
Buffer一般是与Channel配合起来用,Channel读数据的时候,会先读到Buffer里,写数据的时候,也会先写到Buffer里。
- 初始化
以ByteBuffer为例。它们有两个工厂方法allocate和allocateDirect,用于初始化和申请内存。前面提到了在操作IO时,通常使用直接内存,所以一般是这样初始化:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
可以用isDirect()方法来判断当前Buffer对象是否使用了直接内存。
- 写数据,往Buffer中写数据主要有两种方式
- 从Channel写到Buffer
- 从数组写到Buffer
从Channel写到Buffer用的是Channel的read(Buffer buffer)方法,而从数组写到Buffer,主要用的是Buffer的put方法。
// 获取Channel里面的数据并写到buffer
// 返回的是读的位置,也就是buffer的position
int readBytes = socketChannel.read(buffer);
// 从byte数组写到Buffer
buffer.put("hi, 这是client".getBytes(StandardCharsets.UTF_8));
- 切换模式
Buffer分为读模式和写模式,可以通过flip()方法转换模式。事实上,查看这个方法源码,发现flip方法也只是对三个指针进行了操作而已。
public Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
- 读数据,读数据也有两种方式
- 从Buffer读到Channel
- 从Buffer读到数组
读数据会从position读到limit的位置。
// 读取buffer的数据并写入channel
socketChannel.write(buffer);
// 把buffer里面的数据读到byte数组
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, StandardCharsets.UTF_8);
这里用到了Buffer的remaining()方法。这个方法是告诉我们需要读多少字节,方法源码:
public final int remaining() {
return limit - position;
}
- 清空
一般来说,一个Channel用一个Buffer,但Buffer可以重复使用,尤其是对于一些比较大的IO传输内容来说(比如文件),clear()与compact()方法可以重置Buffer。它们有一些微小的区别。
对于clear方法来说,position将被设回0,limit被设置成 capacity的值。
compact方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素后面一位。limit属性依然像clear方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
一般来说,用clear方法的场景会多一点。
源码:
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
public ByteBuffer compact() {
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
try {
UNSAFE.copyMemory(ix(pos), ix(0), (long)rem << 0);
} finally {
Reference.reachabilityFence(this);
}
position(rem);
limit(capacity());
discardMark();
return this;
}
Buffer简单的使用例子
- 从字符串到Channel
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 从字符串写到Buffer
buffer.put("hi, 这是client".getBytes(StandardCharsets.UTF_8));
buffer.flip(); // 转换模式
// 从Buffer写到Channel
socketChannel.write(buffer);
- 从Channel到字符串
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 从Channel写到Buffer
int readBytes = socketChannel.read(buffer);
if (readBytes > 0) {
buffer.flip(); // 转换模式
byte[] bytes = new byte[buffer.remaining()];
// 从Buffer写到字节数组
buffer.get(bytes);
String body = new String(bytes, StandardCharsets.UTF_8);
System.out.println("server 收到:" + body);
}
本文深入探讨Java NIO中的Buffer概念,解析其内部结构、属性及常用API,包括初始化、写入、读取和模式切换等操作,适用于对Java NIO感兴趣的开发者。
1065

被折叠的 条评论
为什么被折叠?



