Buffer 介绍
Buffer 介绍
Java NIO 中的 Buffer 用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该内存块。缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中;在写入数据时,它也是写到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流 I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。
在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer,对于 Java 中的基本类型,都有一个与之对应的 Buffer 类型,它们之间的继承关系如下:

Buffer 的基本用法
- 使用 Buffer 读写数据,一般遵循以下四个步骤:
- 写入数据到 Buffer
- 调用 flip() 方法
- 从 Buffer 中读取数据
- 调用 clear() 方法或者 compact() 方法
当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。一旦读完了所有数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区;compact() 方法只会清除已经读过的数据,任何未读的数据都被转移到缓冲区起始处,新写入的数据将放到缓冲区未读数据的后面。
- 使用 Buffer 的例子
public class ByteBufferDemo {
public static void main(String[] args) throws Exception {
RandomAccessFile file = new RandomAccessFile("E:\\1.txt", "rw");
FileChannel = file.getChannel();
// 定义一个缓冲区,大小为 48 字节
ByteBuffer buffer = ByteBuffer.allocate(48);
// 将 Channel 中的数据读取到 buffer 中
int read = inChannel.read(buffer);
while (read != -1) {
buffer.flip();
// 判断当前位置和限制之间是否有任何元素,是否还有未读数据
while (buffer.hasRemaining()) {
// 获取并打印数据
System.out.print((char) buffer.get());
}
// 清空缓冲区
buffer.clear();
read = inChannel.read(buffer);
}
// 关闭文件流
file.close();
}
}
- 使用 IntBuffer 的例子
public class IntBufferDemo {
public static void main(String[] args) {
// 分配新的 int 缓冲区,参数为缓冲区容量
// 新缓冲区的当前位置(position)将为零,其界限(限制位置 limit)将为其容量
// 它将具有一个底层实现数组,其数组偏移量将为零
IntBuffer buffer = IntBuffer.allocate(8);
for (int i = 0; i < buffer.capacity(); i++) {
int j = 2 * (i + 1);
// 将给定证书写入此缓冲区的当前位置,当前位置(position)递增
buffer.put(j);
}
// 重置缓冲区,将限制位(limit)设置为当前位置,然后将当前位置(position)设置为零
buffer.flip();
// 查看在当前位置和限制之间是否存在元素
while (buffer.hasRemaining()) {
int i = buffer.get();
System.out.print(i + " ");
}
}
}
Buffer 的 capacity、position 和 limit
为了理解 Buffer 的工作原理,需要熟悉它的三个属性:
- Capacity
- Position
- limit
position 和 limit 的含义取决于 Buffer 处在读模式还是写模式。不管 Buffer 处在什么模式,capacity 的含义总是一样的。
这里有一个关于 capacity、position 和 limit
在读写模式中的说明:

capacity
作为一个内存块,Buffer 有一个固定的大小值,也叫做
“capacity”
,它表示你只允许向 Buffer 中写入 capacity 个 byte、long、char 等类型的数据。一旦 Buffer 的容量到达了 capacity 的预期值,则需要将其清空(通过读取数据或者清空缓冲区)才能继续写入数据。position
写数据到 Buffer 中时,
position
表示写入数据的当前位置,position 的初始值为 0。当一个 byte、long 等数据写到 Buffer 后,position 会向下移动到下一个可插入数据的 Buffer 单元。position 的最大值是 capacity - 1(position 的初始值是 0)。读数据到 Buffer 中时,position 表示读入数据的当前位置,如 position = 2 时表示已开始读入了 3 个 byte,或从第 3 个 byte 开始读取。通过 ByteBuffer.flip() 切换到读模式时 position 会被重置为 0,当 Buffer 从 position 读入数据后,position 会下移到下一个可读入的数据 Buffer 单元。
limit
写数据时,
limit
表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于 Buffer 的 capacity。读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读到之前写入的所有数据(limit 在读模式下的值就是写模式下 position 的值)。
Buffer 类型
Java NIO 有以下 Buffer 类型
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些 Buffer 类型代表了不同的数据类型。换句话说,就是可以通过 char、short、int、long、float 或 double 类型来操作缓冲区中的字节。
Buffer 分配和写数据
Buffer 分配
要想获得一个 Buffer 对象首先要进行存储大小分配。每一个 Buffer 类都有一个 allocate 方法。
下面是一个分配 48 字节 capacity 的 ByteBuffer 的案例:
ByteBuffer buf = ByteBuffer.allocate(48);
分配一个可存储 1024 个字符的 CharBuffer:
CharBuffer buf = CharBuffer.allocate(1024);
向 Buffer 中写数据
写数据到 Buffer 有两种方式:
- 从 Channel 写到 Buffer
- 通过 Buffer 的
put()
方法写到 Buffer 里
从 Channel 写到 Buffer 的例子:
int read = inChannel.read(buf);
通过 put() 方法写 Buffer:
buf.put(1237);
put() 方法有很多重载方法,允许你以不同的方式把数据写入到 Buffer 中。例如,写到一个指定的位置,或者把一个字节数组写入到 Buffer。
flip() 方法
flip()
方法将 Buffer 从写模式切换到读模式。调用 flip() 方法会将 position 设置为 0,并将 limit 设置成之前 position 的值。换句话说,position 现在用于标记读的位置,limit 表示之前写进了多少个 byte、char 等数据(或者现在能读取多少个 byte、char 等数据)。
从 Buffer 中读取数据
从 Buffer 中读取数据有两种方式:
- 从 Buffer 读取数据到 Channel
- 使用
get()
方法从 Buffer 中读取数据
从 Buffer 读取数据到 Channel 的例子:
int byteWritten = inChannel.write(buf);
使用 get() 方法从 Buffer 中读取数据的例子
byte aByte = buf.get();
get() 方法有很多重载方法,允许你以不同的方式从 Buffer 中读取数据。例如:从指定 position 读取,或者从 Buffer 中读取数据到字节数组。
Buffer 的方法
rewind() 方法
Buffer.rewind() 将 position 设回 0,所以你可以重读 Buffer 中的所有数据。limit 保持不变,仍然表示能从 Buffer 中读取多少个元素(byte、char 等)。
clear() 与 compact() 方法
一旦读完 Buffer 中的数据,需要让 Buffer 准备好再次被写入。可以通过
clear()
或compact()
方法来完成。如果调用的是 clear() 方法,position 将被设置为 0,limit 被设置成 capacity 的值。换句话说,Buffer 被清空了。Buffer 中的数据并未清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据。
如果 Buffer 中仍有未读完的数据,且后续还需要这些数据,但是此时想要先写数据,那么使用 compact() 方法。
compact() 方法将所有未读完的数据拷贝到 Buffer 起始处。然后将 position 设到最后一个未读元素正后面。limit 属性依然像 clear() 方法一样,设置成 capacity。现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。
mark() 与 reset() 方法
通过调用 Buffer.mark() 方法,可以标记 Buffer 中的一个特定 position。之后可以通过调用 Buffer.reset() 方法恢复到这个 position。例如:
// 标记 buf.mark(); // buf.get(); 读取数据 // 回退到标记点,上一次读取的数据仍然可以读取到 buf.reset();
缓冲区操作
缓冲区分片
在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。调用 slice() 方法可以创建一个子缓冲区。
public class BufferDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
// 填充数据
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte) i);
}
// 创建子缓冲区
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
// 改变子缓冲区的内容
for (int i = 0; i < slice.capacity(); i++) {
byte b = slice.get(i);
b *= 10;
slice.put(i, b);
}
buffer.position(0);
buffer.limit(buffer.capacity());
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
}
}
只读缓冲区
只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的 asReadOnlyBuffer()
方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并于原缓冲区共享数据,只不过它是只读的。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化。
public class ReadOnlyBuffer {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
// 向缓冲区写入数据
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte) i);
}
// 创建只读缓冲区
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
// 改变原缓冲区内容
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.get(i);
b *= 10;
buffer.put(i, b);
}
readOnlyBuffer.position(0);
readOnlyBuffer.limit(readOnlyBuffer.capacity());
// 打印只读缓冲区的内容
for (int i = 0; i < readOnlyBuffer.capacity(); i++) {
System.out.println(readOnlyBuffer.get());
}
}
}
如果尝试修复只读缓冲区的内容,则会报 ReadOnlyBufferException 异常。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读缓冲区转换为可写的缓冲区。
直接缓冲区
直接缓冲区是为了加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区,JDK 文档中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中,或者从一个中间缓冲区中拷贝数据。要分配直接缓冲区,需要调用 allocateDirect()
方法,而不是 allocate()
方法,使用方式与普通缓冲区并无区别。
public class DirectBuffer {
public static void main(String[] args) throws IOException {
RandomAccessFile inFile = new RandomAccessFile("E:\\1.txt", "rw");
FileChannel fcin = inFile.getChannel();
RandomAccessFile outFile = new RandomAccessFile("E:\\4.txt", "rw");
FileChannel fcout = outFile.getChannel();
// 调用 allocateDirect() 获取直接缓冲区空间
ByteBuffer buffer = ByteBuffer.allocateDirect(64);
while (true) {
// 清空缓冲区,准备向缓冲区写入数据
buffer.clear();
int read = fcin.read(buffer);
// -1 时则表示通道内没有可读数据
if (read == -1) {
break;
}
// 翻转缓冲区,准备从缓冲区取出数据
buffer.flip();
fcout.write(buffer);
}
inFile.close();
outFile.close();
}
}
内存映射文件 I/O
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快的多。内存映射文件 I/O 是通过使文件中的数据出现为内存数组的内容来完成的,这听起来像是将文件数写入的内存中,但是事实上并不是这样的。一般来说,只有文件中实际需要读取或写入的部分才会映射到内存中。
public class MemoryMap {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("E:\\1.txt", "rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer map = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
map.put(0, (byte) 97);
map.put(1023, (byte) 122);
raf.close();
}
}
提示
内存映射并没有那么神奇。现代操作系统一般会根据需要将文件的部分内容映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
虽然创建内存映射文件很简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样简单的操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。