Java nio:Buffer总结

前言:

我在今年的5月份学习过nio,可是也局限在用用简单的api和仿照并发网的例子,过了大半年,几乎99%的东西都忘记的一干二净,在月末开始学习netty,不得不重新温习这些很重要的内容,结合第一次的学习例子和本次查找的资料,写下总结和思考吧。

缓冲区Buffer:此部分引用博客1博客2内容
  1. 状态变量:
    状态变量是前一节中提到的”内部统计机制”的关键。 每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。每一种Java基本类型的缓冲区都是抽象类Buffer的子类,从Buffer的源代码中可以发现,它定义了三个私有属性:
    private int position = 0;  
    private int limit;  
    private int capacity;

每一个基本类型的缓冲区底层实际上就是一个该类型的数组。在从通道读取时,所读取的数据将放被到底层的数组中;同理,向通道中写入时,将从底层数组中将数据写入通道。
a) position
position变量跟踪了向缓冲区中写入了多少数据或者从缓冲区中读取了多少数据。更确切的说,当您从通道中读取数据到缓冲区中时,它指示了下一个数据将放到数组的哪一个元素中。比如,如果您从通道中读三个字节到缓冲区中,那么缓冲区的position将会设置为3,指向数组中第4个元素。反之,当您从缓冲区中获取数据进行写通道时,它指示了下一个数据来自数组的哪一个元素。比如,当您从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设置为5,指向数组的第六个元素。
b) limit
limit变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
position总是小于或者等于limit。
c) capacity
capacity变量表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小—或者至少是指定了准许我们使用的底层数组的容量。
limit总是小于或者等于capacity。
d) 举例说明:
下面我们就以数据从一个输入通道拷贝到一个输出通道为例,来详细分析每一个变量,并说明它们是如何协同工作的:
初始变量:
我们首先观察一个新创建的缓冲区,以ByteBuffer为例,假设缓冲区的大小为8个字节,ByteBuffer初始状态如下:
这里写图片描述
回想一下 ,limit决不能大于capacity,此例中这两个值都被设置为8。我们通过将它们指向数组的尾部之后(第8个槽位)来说明这点。
这里写图片描述
我们再将position设置为0。表示如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自slot 0。position设置如下所示:这里写图片描述
第一次读取:
现在我们可以开始在新创建的缓冲区上进行读/写操作了。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从position开始的位置,这时position被设置为0。读完之后,position就增加到了3,如下所示,limit没有改变。
这里写图片描述
第二次读取:
在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由position所指定的位置上, position因而增加2,limit没有改变。
这里写图片描述

flip: 现在我们要将数据写到输出通道中。在这之前,我们必须调用flip()方法。 其源代码如下:

public final Buffer flip() {  
    limit = position;  
    position = 0;  
    mark = -1;  
    return this;  
} 

这个方法做两件非常重要的事:
i 它将limit设置为当前position。
ii 它将position设置为0。
上一个图显示了在flip之前缓冲区的情况。下面是在flip之后的缓冲区:
这里写图片描述
我们现在可以将数据从缓冲区写入通道了。position被设置为0,这意味着我们得到的下一个字节是第一个字节。limit已被设置为原来的position,这意味着它包括以前读到的所有字节,并且一个字节也不多。
第一次写入:
在第一次写入时,我们从缓冲区中取四个字节并将它们 写入输出通道。这使得position增加到4,而limit不变,如下所示:
这里写图片描述
第二次写入:
我们只剩下一个字节可写了。limit在我们调用flip()时被设置为5,并且position不能超过limit。 所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得position增加到5,并保持limit不变,如下所示:
这里写图片描述
clear:
最后一步是调用缓冲区的clear()方法。这个方法重设缓冲区以便接收更多的字节。其源代码如下:

public final Buffer clear() {  
    osition = 0;  
    limit = capacity;  
    mark = -1;  
    return this;  
}  

clear做两种非常重要的事情:
i 它将limit设置为与capacity相同。
ii 它设置position为0。
下图显示了在调用clear()后缓冲区的状态, 此时缓冲区现在可以接收新的数据了。
这里写图片描述
2. 访问方法:
到目前为止,我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。 或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。 实际上,每一个基本类型的缓冲区都为我们提供了直接访问缓冲区中数据的方法,我们以ByteBuffer为例,分析如何使用其提供的get()和put()方法直接访问缓冲区中的数据。
a) get()
ByteBuffer类中有四个get()方法:

byte get();  
ByteBuffer get( byte dst[] );  
ByteBuffer get( byte dst[], int offset, int length );  
byte get( int index );

第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回ByteBuffer的方法只是返回调用它们的缓冲区的this值。
此外,我们认为前三个get()方法是相对的,而最后一个方法是绝对的。“相对”意味着get()操作服从limit和position值,更明确地说,字节是从当前position读取的,而position在get之后会增加。另一方面,一个“绝对”方法会忽略limit和position值,也不会影响它们。事实上,它完全绕过了缓冲区的统计方法。
上面列出的方法对应于ByteBuffer类。其他类有等价的get()方法,这些方法除了不是处理字节外,其它方面是是完全一样的,它们处理的是与该缓冲区类相适应的类型。
b) put()
ByteBuffer类中有五个put()方法:

ByteBuffer put( byte b );  
ByteBuffer put( byte src[] );  
ByteBuffer put( byte src[], int offset, int length );  
ByteBuffer put( ByteBuffer src );  
ByteBuffer put( int index, byte b ); 

第一个方法 写入(put)单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将数据从一个给定的源ByteBuffer写入这个ByteBuffer。第五个方法将字节写入缓冲区中特定的 位置 。那些返回ByteBuffer的方法只是返回调用它们的缓冲区的this值。与get()方法一样,我们将把put()方法划分为“相对”或者“绝对”的。前四个方法是相对的,而第五个方法是绝对的。上面显示的方法对应于ByteBuffer类。其他类有等价的put()方法,这些方法除了不是处理字节之外,其它方面是完全一样的。它们处理的是与该缓冲区类相适应的类型。
c) 类型化的 get() 和 put() 方法
除了前些小节中描述的get()和put()方法, ByteBuffer还有用于读写不同类型的值的其他方法,如下所示:
java
getByte()
getChar()
getShort()
getInt()
getLong()
getFloat()
getDouble()
putByte()
putChar()
putShort()
putInt()
putLong()
putFloat()
putDouble()

事实上,这其中的每个方法都有两种类型:一种是相对的,另一种是绝对的。它们对于读取格式化的二进制数据(如图像文件的头部)很有用。
3) 如何使用?
下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。

while (true) {  
     buffer.clear();  
     int r = fcin.read( buffer ); 
     if (r==-1) {  
       break;  
     }  
     buffer.flip();  
     fcout.write( buffer );  
}  

read()和write()调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。clear()和flip()方法用于让缓冲区在读和写之间切换。

PS:加上一段关于四个成员英文解释:

Capacity
The maximum number of data elements the buffer can hold. The capacity is set when
the buffer is created and can never be changed.
Limit
The first element of the buffer that should not be read or written. In other words, the count of live elements in the buffer.
Position
The index of the next element to be read or written. The position is updated automatically by relative get( ) and put( ) methods.
Mark
A remembered position. Calling mark( ) sets mark = position. Calling reset( ) sets position = mark. The mark is undefined until set.
The following relationship between these four attributes always holds:
0 <= mark <= position <= limit <= capacity

buffer的包装:

1) 缓冲区分配和包装
在能够读和写之前,必须有一个缓冲区。要创建缓冲区,您必须“分配”它。我们使用静态方法allocate()来分配缓冲区:

ByteBuffer buffer = ByteBuffer.allocate( 1024 );  

allocate()方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中,在本例中是一个ByteBuffer。
您还可以将一个现有的数组转换为缓冲区,如下所示:

byte array[] = new byte[1024];  
ByteBuffer buffer = ByteBuffer.wrap( array );  

本例使用了wrap()方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装,底层数据就可以通过缓冲区或者直接访问。

2) 缓冲区分片
slice()方法根据现有的缓冲区创建一个子缓冲区。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。
使用例子可以最好地说明这点。让我们首先创建一个长度为10的ByteBuffer:

ByteBuffer buffer = ByteBuffer.allocate( 10 );

然后使用数据来填充这个缓冲区,在第n个槽中放入数字n:

for (int i=0; i<buffer.capacity(); ++i) {  
     buffer.put( (byte)i );  
}

现在我们对这个缓冲区“分片”,以创建一个包含槽3到槽6的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个窗口 。
窗口的起始和结束位置通过设置position和limit值来指定,然后调用Buffer的slice()方法进行分片:

buffer.position( 3 );  
buffer.limit( 7 );  
ByteBuffer slice = buffer.slice(); 

该“片段”是缓冲区的子缓冲区。不过,“片段”和“缓冲区”共享同一个底层数据数组,我们在下一节将会看到这一点。
3) 缓冲区片份和数据共享
我们已经创建了原缓冲区的子缓冲区,并且已经知道缓冲区和子缓冲区共享同一个底层数据数组。让我们看看这意味着什么。
我们遍历子缓冲区,将每一个元素乘以11来改变它。例如,5会变成55。

for (int i=0; i<slice.capacity(); ++i) {  
     byte b = slice.get( i );  
     b *= 11;  
     slice.put( i, b );  
} 

代码截图和运行结果截图:
这里写图片描述
这里写图片描述
但是发生了异常:
这里写图片描述
原因是:窗口的起始和结束位置通过设置position和limit值来指定,如果我们用的是下面的代码结果如下图。
这里写图片描述
改为:

buffer.position( 0 );  
buffer.limit( buffer.capacity() );   
while (buffer.remaining()>0) {  
     System.out.println( buffer.get() );  
}  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值