Java NIO 緩衝區學習筆記

xiaoluo501395377發表於2016-08-01

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

相關文章