Java NIO 系列学习 04 - Buffers

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

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

基础使用 Link to heading

使用 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) Link to heading

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

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

  • Capacity
  • Position
  • Limit

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

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

Capacity Link to heading

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

Position Link to heading

要分为两种情况来看。

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

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

Limit Link to heading

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

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

Buffer 的类型 Link to heading

Java NIO 包含了下面这些 Buffer 类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

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

为Buffer分配大小 Link to heading

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

ByteBuffer buf = ByteBuffer.allocate(48);

写数据到Buffer Link to heading

有两种方式可以把数据写入到Buffer中:

  1. 从 Channel 中把读出数据写到 Buffer中
  2. 通过 Buffer提供的put()方法把数据写到 Buffer中

方式一示例:

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

方式二示例:

buf.put(42);

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

flip() 切到读模式 Link to heading

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

从Buffer读数据 Link to heading

有两种方式可以从Buffer中读取数据:

  1. 从buffer中读数据到channel
  2. 通过Buffer提供的get()方法来读

方式一示例:

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

方式二示例:

byte aByte = buf.get();

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

rewind() Link to heading

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

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

clear() and compact() Link to heading

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

clear() Link to heading

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

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

compact() Link to heading

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

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

mark() and reset() Link to heading

可以通过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() Link to heading

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

equals() Link to heading

什么条件下两个Buffer是相等(equals)的呢?

  1. 存储的数据类型是一致的(byte, char, int 等等)
  2. 剩余可用数据大小相同
  3. 剩余可用数据类型相同

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

compareTo() Link to heading

compareTo() 方法比较两个Buffer的剩余元素,在排序时会用到。
什么条件下一个Buffer会比另一个Buffer更小呢?

  1. A Buffer的第一个元素小于B Buffer第一个相同元素
  2. 所有元素相等,但A Buffer比B Buffer更早的读取完数据,也就是说A元素少

参考 Link to heading

  1. Java NIO Buffer