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).