Buffer其實就是一個容器對象,它包含一些要寫入或剛讀出的資料。在NIO中加入Buffer對象,體現了新函式庫與原I/O的一個重要差異。在面向流的I/O中,您將資料直接寫入或將資料直接讀到Stream物件中。
在NIO函式庫中,所有資料都是用緩衝區處理的。在讀取資料時,它是直接讀到緩衝區中的。在寫入資料時,它是寫入到緩衝區中的。任何時候存取NIO中的數據,您都是將它放到緩衝區中。
緩衝區實質上是一個陣列。通常它是一個位元組數組,但是也可以使用其他種類的數組。但是一個緩衝區不只是一個陣列。緩衝區提供了對資料的結構化訪問,而且還可以追蹤系統的讀取/寫入進程。
最常用的緩衝區類型是ByteBuffer。 一個ByteBuffer可以在其底層位元組數組上進行get/set操作(即位元組的取得和設定)。
ByteBuffer不是NIO中唯一的緩衝區類型。事實上,對於每一種基本Java類型都有一種緩衝區類型(只有boolean類型沒有其對應的緩衝區類別):
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
每一個Buffer類別都是Buffer介面的一個實例。 除了ByteBuffer, 每個Buffer類別都有完全一樣的操作,只是它們所處理的資料型別不一樣。因為大多數標準I/O操作都使用ByteBuffer,所以它具有所有共享的 緩衝區操作以及一些特有的操作。讓我們來看看Buffer的類別層次圖吧:
每個Buffer 都有以下的屬性:
capacity
這個Buffer 最多可以放多少數據。 capacity 一般在 buffer 被創建的時候指定。
limit
在 Buffer 上進行的讀寫操作都不能越過這個下標。當寫入資料到 buffer 中時, limit 一般和 capacity 相等,當讀取資料時, limit 代表 buffer 中有效資料的長度。
position
position變數追蹤了向緩衝區中寫入了多少資料或從緩衝區中讀取了多少資料。
更確切的說,當您從通道中讀取資料到緩衝區時,它指示了下一個資料將放到數組的哪一個元素中。例如,如果您從通道中讀三個位元組到緩衝區中,那麼緩衝區的 position將會設為3,指向數組中第4個元素。反之,當您從緩衝區中獲取資料進行寫入通道時,它指示了下一個資料來自數組的哪一個元素。例如,當您 從緩衝區寫了5個位元組到通道中,那麼緩衝區的 position 將被設定為5,指向數組的第六個元素。
mark
一個暫時存放的位置下標。呼叫 mark() 會將 mark 設為目前的 position 的值,以後呼叫 reset() 會將 position 屬性設為 mark 的值。 mark 的值總是小於等於 position 的值,如果將 position 的值設的比 mark 小,目前的 mark 值會被拋棄掉。
這些屬性總是滿足以下條件:
0 <= mark <= position <= limit <= capacity
緩衝區的內部實作機制:
下面我們就以資料從一個輸入通道拷貝到一個輸出通道為例,來詳細分析每一個變量,並說明它們是如何協同工作的:
初始變數:
我們先觀察一個新建立的緩衝區,以ByteBuffer為例,假設緩衝區的大小為8個位元組,ByteBuffer初始狀態如下:
回想一下,limit絕不能大於capacity,此例中這兩個值都被設定為8。我們透過將它們指向數組的尾部之後(第8個槽位)來說明這點。
我們再將position設定為0。表示如果我們讀一些資料到緩衝區中,那麼下一個讀取的資料就進入 slot 0。如果我們從緩衝區寫一些數據,從緩衝區讀取的下一個位元組就來自slot 0。 position設定如下所示:
由於緩衝區的最大資料容量capacity不會改變,所以我們在下面的討論中可以忽略它。
第一次讀取:
現在我們可以開始在新建立的緩衝區上進行讀取/寫入操作了。首先從輸入通道中讀一些資料到緩衝區。第一次讀取得到三個位元組。它們被放到數組中從 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()後緩衝區的狀態, 此時緩衝區現在可以接收新的資料了。
至此,我們只是使用緩衝區將資料從一個通道轉移到另一個通道,然而,程式經常需要直接處理資料。例如,您可能需要將用戶資料儲存到磁碟。在這種情況下,您必須將這些資料直接放入緩衝區,然後用通道將緩衝區寫入磁碟。 或者,您可能想要從磁碟讀取用戶資料。在這種情況下,您要將資料從頻道讀取緩衝區中,然後檢查緩衝區中的資料。實際上,每一個基本類型的緩衝區都為我們提供了直接存取緩衝區中資料的方法,我們以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()方法,这些方法除了不是处理字节外,其它方面是是完全一样的,它们处理的是与该缓冲区类相适应的类型。
注:这里我们着重看一下第二和第三这两个方法
ByteBuffer get( byte dst[] ); ByteBuffer get( byte dst[], int offset, int length );
这两个get()主要用来进行批量的移动数据,可供从缓冲区到数组进行的数据复制使用。第一种形式只将一个数组 作为参数,将一个缓冲区释放到给定的数组。第二种形式使用 offset 和 length 参数来指 定目标数组的子区间。这些批量移动的合成效果与前文所讨论的循环是相同的,但是这些方法 可能高效得多,因为这种缓冲区实现能够利用本地代码或其他的优化来移动数据。
buffer.get(myArray)
等价于:
buffer.get(myArray,0,myArray.length);
注:如果您所要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不 变,同时抛出 BufferUnderflowException 异常。因此当您传入一个数组并且没有指定长度,您就相当于要求整个数组被填充。如果缓冲区中的数据不够完全填满数组,您会得到一个 异常。这意味着如果您想将一个小型缓冲区传入一个大数组,您需要明确地指定缓冲区中剩 余的数据长度。上面的第一个例子不会如您第一眼所推出的结论那样,将缓冲区内剩余的数据 元素复制到数组的底部。例如下面的代码:
String str = "com.xiaoluo.nio.MultipartTransfer"; ByteBuffer buffer = ByteBuffer.allocate(50); for(int i = 0; i < str.length(); i++) { buffer.put(str.getBytes()[i]); } buffer.flip();byte[] buffer2 = new byte[100]; buffer.get(buffer2); buffer.get(buffer2, 0, length); System.out.println(new String(buffer2));
这里就会抛出java.nio.BufferUnderflowException异常,因为数组希望缓存区的数据能将其填满,如果填不满,就会抛出异常,所以代码应该改成下面这样:
//得到缓冲区未读数据的长度 int length = buffer.remaining(); byte[] buffer2 = new byte[100]; buffer.get(buffer2, 0, length); 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还有用于读写不同类型的值的其他方法,如下所示:
getByte()
getChar()
getShort()
getInt()
getLong()
getFloat()
getDouble()
putByte()
putChar()
putShort()
putInt()
putLong()
putFloat()
putDouble()
事实上,这其中的每个方法都有两种类型:一种是相对的,另一种是绝对的。它们对于读取格式化的二进制数据(如图像文件的头部)很有用。
下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。
while(true) { //clear方法重设缓冲区,可以读新内容到buffer里 buffer.clear(); int val = inChannel.read(buffer); if(val == -1) { break; } //flip方法让缓冲区的数据输出到新的通道里面 buffer.flip(); outChannel.write(buffer); }
read()和write()调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。clear()和flip()方法用于让缓冲区在读和写之间切换。
好了,缓冲区的内容就暂且写到这里,下一篇我们将继续NIO的学习–通道(Channel).
以上就是Java NIO 缓冲区学习笔记 的内容,更多相关内容请关注PHP中文网(www.php.cn)!