Java NIO 系列学习 04 - Buffers

Java NIO Buffers 是与 Channels一起组合使用的。

Buffer本质上是一块内存区,我们可以写入数据,然后再读出来。这个内存区被包装为 NIO Buffer 对象, 这个对象提供了一组方法以方便我们操作内存区。

基础使用

使用 Buffer 读、写数据通常有下面4个步骤:
1. 写数据到Buffer 2. 调用 buffer.flip() 3. 从Buffer读出数据 4. 调用 buffer.clear() 或者是 buffer.compact()

当我们把数据写到 buffer 时,buffer会保持跟踪写了多少数据进去。一旦需要读数据,就需要调用 flip()方法来转换 buffer 从写模式切到读模式。
在读模式下,buffer 可以让我们读到所有刚才写入的数据。

一旦我们读取了所有数据,就需要 clear buffer, 以便于再次转换为写模式,有两种方式可以实现cleat buffer:
1. 调用 clear() 2. 调用 compact()

需要说明一下,clear() 会清除整个buffer;compact() 仅清除已经读过的数据,未读的数据会被移动到buffer的头部,而且数据可以写入到未读数据的后面。

下面是一个简单的 Buffer 使用示例。

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();

Buffer 容量(Capacity), 位置(Position) and 限制(Limit)

Buffer本质上是一块内存区,我们可以写入数据,然后再读出来。这个内存区被包装为 NIO Buffer 对象, 这个对象提供了一组方法以方便我们操作内存区。

为了了解Buffer的工作原理,我们需要熟悉它的三个属性:
- Capacity - Position - Limit

PositionLimit的含义依赖于Buffer所处于的是读模式还是写模式。Capacity在读写模式中都是一样的。

下面这张示例图说明了读写模式下三个属性的说明:
Buffer capacity, position and limit in write and read mode.

Capacity

Buffer 作为内存块, 有一个确定的大小, 叫做 capacity. 我们仅能写 capacity bytes,longs,chars等类型数据到Buffer中。一旦Buffer被写满了,如果要继续写就需要清空Buffer。

Position

要分为两种情况来看。

写模式:当我们写数据到Buffer时,是从一个确定的position开始,初始标记为 0。当有数据写入到Buffer时,position 会提前的指向Buffer的下一个cell以便于插入数据。position 最大是 capacity - 1.

读模式:当我们从Buffer中读数据时,同样是从一个确定的position开始。当我们把Buffer的模式从写模式切(flip)到读模式时,position 就会重置为 0。当我们从Buffer中读数据时,是从 position 指向的数据读出来的,同时position会提前指向下一个position来读数据。

Limit

写模式:在写模式下,limit 是指每次能写多少数据到buffer中。换句话说,写模式下 limit 等于 capacity.

读模式:当切换Buffer到读模式下时,limit表示每次能读到的数据的大小。因此,limit 等于写模式下的 position。换句话说,我们读多少数据取决于写入了多少。再换句话说,limit 等于写入数据的大小(number of bytes), 这个大小是标记为position的。

Buffer 的类型

Java NIO 包含了下面这些 Buffer 类型:
- ByteBuffer - MappedByteBuffer - CharBuffer - DoubleBuffer - FloatBuffer - IntBuffer - LongBuffer - ShortBuffer

这些Buffer类型分别表示类不同数据类型。MappedByteBuffer 有些特殊,后面再讲。

为Buffer分配大小

先分配才能获取buffer。每一个Buffer类都有一个allocate()方法来分配。
举个例子,为 ByteBuffer 分配48 bytes大小的容量(capacity)

ByteBuffer buf = ByteBuffer.allocate(48);

写数据到Buffer

有两种方式可以把数据写入到Buffer中:
1. 从 Channel 中把读出数据写到 Buffer中 2. 通过 Buffer提供的put()方法把数据写到 Buffer中

方式一示例:

int bytesRead = inChannel.read(buf); //read into buffer.

方式二示例:

buf.put(42);

有很多其他类型的put()方法来写数据到Buffer中。这里不再赘述

flip() 切到读模式

flip() 方法是用来实现从写模式切换到读模式的。当调用flip()方法时,会把position重置为0,同时会把limit置为 position 重置为0之前的位置。换句话说,limit 现在表示的是写入的数据大小——有多少数据可以被读出来。

从Buffer读数据

有两种方式可以从Buffer中读取数据:
1. 从buffer中读数据到channel 2. 通过Buffer提供的get()方法来读

方式一示例:

int bytesWritten = inChannel.write(buf); //read from buffer into channel.

方式二示例:

byte aByte = buf.get();

有很多其他类型的get()方法来实现从buffer中读数据。这里不再赘述

rewind()

这个方法Buffer.rewind(), 可以把position重置为0,因此可以从Buffer中读取所有数据。limit 保持不变,仍然表示有多少数据可以从Buffer中读取。

PS: 注意下flip()rewind()的区别。两者都会把position置为0,但flip()会把limit置为position重置为0之前的位置,而rewind()不会对limit有改变。

clear() and compact()

在我们从Buffer中读取数据之后,就需要把Buffer置为可写模式,可以通过调用clear()或者compact()来实现。

clear()

调用clear()方法后,position会被置为0, limit被置为与capacity相同大小。事实上此时数据并未被清除,只是被标记为了可写。

调用clear()时如果还有未被读取的数据,则这部分数据就再也没有机会被读到了,因为并不清楚这部分数据时从哪里开始的。

compact()

当然了,如果有需求希望把Buffer转换为写模式而又不丢失未被读取的数据时,可以使用compact()方法。

compact() 会复制所有未被读取的数据到Buffer的头部,然后会重置position到最后一个未被读取数据的地方,limit 同样的会被置为与capacity相同大小。

mark() and reset()

可以通过mark()方法来标记Buffer的position值。当往后读取了一部分后,可以通过调用reset()方法来回到上一步标记的地方(position会重新置为标记的地方)。

buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset();  //set position back to mark.

equals() and compareTo()

怎么比较两个Buffer是否相等呢?可以使用equals()或者compareTo()方法。

equals()

什么条件下两个Buffer是相等(equals)的呢?
1. 存储的数据类型是一致的(byte, char, int 等等) 2. 剩余可用数据大小相同 3. 剩余可用数据类型相同

equals() 只比较Buffer的一部分,并不比较每一个元素,而是只对比剩余元素。

compareTo()

compareTo() 方法比较两个Buffer的剩余元素,在排序时会用到。
什么条件下一个Buffer会比另一个Buffer更小呢?
1. A Buffer的第一个元素小于B Buffer第一个相同元素 2. 所有元素相等,但A Buffer比B Buffer更早的读取完数据,也就是说A元素少

参考

  1. Java NIO Buffer