理解NIO中的缓冲区,首先需要了解缓冲区两个重要的缓冲区组件:状态变量和访问方法 (Accessor)。
状态变量是NIO缓存对象的"内部统计机制"的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。
在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这时可以使用get()来完成的。同样,如果要将原始数据放入缓冲区中,可以使用put()方法来完成。
一、状态变量定义
缓冲区使用三个值指定缓冲区在任意时刻的状态,如下:
1、position
您可以回想一下,缓冲区实际上就是一个数组。在从通道读取时,您将所读取的数据放到底层的数组中。 position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素(下标从0开始)。
同样,在写入通道时,您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设置为5,指向数组的第六个元素。
2、limit
该变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
position总是小于或者等于limit。
3、capacity
缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小或者至少是指定了准许我们使用的底层数组的容量。
limit决不能大于capacity。
注意:
标记、位置、限制和容量值遵守以下不变式:
0 <= 标记 <= 位置 <= 限制 <= 容量
二、状态变化详解
首先新创建一个缓冲区。我们假设这个缓冲区的总容量为8个字节。 Buffer的状态如下所示:
回想一下,limit决不能大于capacity,此例中这两个值都被设置为8。我们通过将它们指向数组的尾部之后(如果有第8个字节,则是第8个字节所在的位置)来说明这点。
position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot0 。 position 设置如下所示:
由于 capacity 不会改变,所以我们在下面的讨论中可以忽略它。
2.1 第一次读取
现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从position开始的位置(position=0)。读完之后position=3,如下所示:
2.2 第二次读取
在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由position=3指定的开始位置,因而增加position=5,如下图所示:
三、flip方法
现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip() 方法。这个方法做两件非常重要的事:
1、它将limit设置为当前 position。
2、它将position设置为 0。
调用flip方法之后的缓冲区变量的状态如下图所示:
我们现在可以将数据从缓冲区写入通道了。 position被设置为0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的position,这意味着它包括以前读到的所有字节。
3.1 第一次写入
在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position增加到4,而limit不变,如下所示:
3.2 第二次写入
我们只剩下一个字节可写了。 limit在我们调用 flip() 时被设置为 5,并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得 position 增加到 5,并保持limit不变,如下所示:
四、clear方法
最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情:
1、它将 limit 设置为与 capacity 相同。
2、它设置 position 为 0。
下图显示了在调用clear()方法后缓冲区的状态:
缓冲区现在可以接收新的数据了。
五、访问方法
到目前为止,我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。 或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。
5.1 get()方法
ByteBuffer 类中有四个 get() 方法:
// 获取单个字节 byte get(); // 将一组字节读到一个数组中 ByteBuffer get( byte dst[] ); // 将一组字节读到一个数组中 ByteBuffer get( byte dst[], int offset, int length ); // 从缓冲区中的特定位置获取字节 byte get( int index );
此外,我们认为前三个 get() 方法是相对的,而最后一个方法是绝对的。 相对意味着get()操作服从limit和 position值;更明确地说,字节是从当前position读取的,而position在get 之后会增加。另一方面,一个绝对方法会忽略limit和position值,也不会影响它们。事实上,它完全绕过了缓冲区的统计方法。
上面列出的方法对应于ByteBuffer类。其他类有等价的get()方法,这些方法除了不是处理字节外,其它方面是是完全一样的,它们处理的是与该缓冲区类相适应的类型。
5.2 put()方法
ByteBuffer 类中有五个 put() 方法:
// 写入(put) 单个字节 ByteBuffer put( byte b ); // 写入来自一个数组的一组字节 ByteBuffer put( byte src[] ); // 写入来自一个数组的一组中指定位置和字节数的字节 ByteBuffer put( byte src[], int offset, int length ); // 将数据从一个给定的源 ByteBuffer 写入这个 ByteBuffer ByteBuffer put( ByteBuffer src ); // 将字节写入缓冲区中特定的位置 ByteBuffer put( int index, byte b );
与 get() 方法一样,我们将把 put() 方法划分为 相对 或者 绝对 的。前四个方法是相对的,而第五个方法是绝对的。
上面显示的方法对应于 ByteBuffer 类。其他类有等价的 put() 方法,这些方法除了不是处理字节之外,其它方面是完全一样的。它们处理的是与该缓冲区类相适应的类型。
除了前些小节中描述的 get() 和 put() 方法, ByteBuffer 还有用于读写不同类型的值的其他方法,如下所示:
getByte()
getChar()
getShort()
getInt()
getLong()
getFloat()
getDouble()
putByte()
putChar()
putShort()
putInt()
putLong()
putFloat()
putDouble()
事实上,这其中的每个方法都有两种类型 — 一种是相对的,另一种是绝对的。它们对于读取格式化的二进制数据(如图像文件的头部)很有用。
六、缓冲区的使用
下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。
while (true) { // 清理缓存状态,可接收新数据 buffer.clear(); // 写入数据到缓冲区 int r = fcin.read( buffer ); if (r==-1) { break; } // 反转缓冲区(position=0,limit=r) buffer.flip(); // 写数据 fcout.write( buffer ); }
read() 和 write() 调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。 clear() 和 flip() 方法用于让缓冲区在读和写之间切换。