Java NIO Buffer
Java IO/">NIO中的Buffer用於和NIO通道進行互動。資料是從通道讀入緩衝區,從緩衝區寫入到通道中的。緩衝區本質上是一塊固定大小的記憶體,其作用是一個儲存器或運輸器。這塊記憶體被包裝成NIO Buffer物件,並提供了一組方法,用來方便的訪問該塊記憶體。
1、緩衝區基礎
理論上,Buffer是經過包裝後的陣列,而Buffer提供了一組通用api對這個陣列進行操作。
1.1、屬性:
容量(Capacity)
緩衝區能夠容納的資料元素的最大數量。這一容量在緩衝區建立時被設定,並且永遠不能被改變。
上界(Limit)
緩衝區的第一個不能被讀或寫的元素。或者說,緩衝區中現存元素的計數。
位置(Position)
下一個要被讀或寫的元素的索引。位置會自動由相應的get( )和put( )函式更新。
標記(Mark)
一個備忘位置。呼叫mark( )來設定mark = postion。呼叫reset( )設定position = mark。標記在設定前是未定義的(undefined)。這四個屬性之間總是遵循以下關係: 0 <= mark <= position <= limit <= capacity 讓我們來看看這些屬性在實際應用中的一些例子。下圖展示了一個新建立的容量為10的ByteBuffer邏輯檢視。

ByteBuffer邏輯檢視.png
位置被設定為0,而且容量和上界被設定為9,超過緩衝區能容納的最後一個位元組,標記初始未定義(-1),當緩衝區建立完畢,則容量就不變,而其他屬性隨讀寫等操作改變。
1.2、快取區API
public final int capacity( ) :返回緩衝區的容量
public final int position( ) :返回緩衝區的位置
public final Buffer position (int newPosition):設定緩衝區的位置
public final int limit( ) :返回緩衝區的上界
public final Buffer limit (int newLimit) :設定緩衝區的上界
public final Buffer mark( ) :設定緩衝區的標記
public final Buffer reset( ) :將緩衝區的位置重置為標記的位置
public final Buffer clear( ) :清除緩衝區
public final Buffer flip( ) :反轉緩衝區
public final Buffer rewind( ) :重繞緩衝區
public final int remaining( ) :返回當為位置與上界之間的元素數
public final boolean hasRemaining( ) :快取的位置與上界之間是否還有元素
public abstract boolean isReadOnly( ):緩衝區是否為只讀緩衝區
1.3、存取
Buffer提供了一組獲取緩衝區的api:
public abstract byte get( ):獲取緩衝區當前位置上的位元組,然後增加位置;
public abstract byte get (int index):獲取指定索引中的位元組;
public abstract ByteBuffer put (byte b):將位元組寫入當前位置的緩衝區中,並增加位置;
public abstract ByteBuffer put (int index, byte b):將自己寫入給定位置的緩衝區中;
Get和put可以是相對的或者是絕對的。在前面的程式列表中,相對方案是不帶有索引引數的函式。當相對函式被呼叫時,位置在返回時前進一。如果位置前進過多,相對運算就會丟擲異常。對於put(),如果運算會導致位置超出上界,就會丟擲BufferOverflowException異常。對於get(),如果位置不小於上界,就會丟擲BufferUnderflowException異常。絕對存取不會影響緩衝區的位置屬性,但是如果您所提供的索引超出範圍(負數或不小於上界),也將丟擲IndexOutOfBoundsException異常。
1.4、翻轉
我們已經寫滿了緩衝區,現在我們必須準備將其清空。我們想把這個緩衝區傳遞給一個通道,以使內容能被全部寫出。但如果通道現在在緩衝區上執行get(),那麼它將從我們剛剛插入的有用資料之外取出未定義資料。如果我們將位置值重新設為0,通道就會從正確位置開始獲取,但是它是怎樣知道何時到達我們所插入資料末端的呢?這就是上界屬性被引入的目的。上界屬性指明瞭緩衝區有效內容的末端。我們需要將上界屬性設定為當前位置,然後將位置重置為0。
我們可以人工用下面的程式碼實現: buffer.limit(buffer.position()).position(0);
但這種從填充到釋放狀態的緩衝區翻轉是API設計者預先設計好的,他們為我們提供了一個非常便利的函式: Buffer.flip();
Flip()函式將一個能夠繼續新增資料元素的填充狀態的緩衝區翻轉成一個準備讀出元素的釋放狀態。
翻轉前:

翻轉前緩衝區檢視.png
翻轉後:

翻轉後緩衝區檢視.png
flip()原始碼如下:
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
Rewind()函式與flip()相似,但不影響上界屬性。它只是將位置值設回0。您可以使用rewind()後退,重讀已經被翻轉的緩衝區中的資料。
rewind()原始碼如下:
public final Buffer rewind() { position = 0; mark = -1; return this; }
1.5、釋放
如果您接收到一個在別處被填滿的緩衝區,您可能需要在檢索內容之前將其翻轉。例如,如果一個通道的read()操作完成,而您想要檢視被通道放入緩衝區內的資料,那麼您需要在呼叫get()之前翻轉緩衝區。通道物件在緩衝區上呼叫put()增加資料;put和read可以隨意混合使用。
布林函式hasRemaining()會在釋放緩衝區時告訴您是否已經達到緩衝區的上界。以下是一種將資料元素從緩衝區釋放到一個數組的方法。
for (int i = 0; buffer.hasRemaining( ), i++) {
myByteArray [i] = buffer.get( );
}
作為選擇,remaining()函式將告知您從當前位置到上界還剩餘的元素數目。如果您對緩衝區有專門的控制,這種方法會更高效,因為上界不會在每次迴圈重複時都被檢查。
緩衝區並不是多執行緒安全的。如果您想以多執行緒同時存取特定的緩衝區,您需要在存取緩衝區之前進行同步。
一旦緩衝區物件完成填充並釋放,它就可以被重新使用了。Clear()函式將緩衝區重置為空狀態。它並不改變緩衝區中的任何資料元素,而是僅僅將上界設為容量的值,並把位置設回0。
1.6 、壓縮
有時,您可能只想從緩衝區中釋放一部分資料,而不是全部,然後重新填充。為了實現這一點,未讀的資料元素需要下移以使第一個元素索引為0。儘管重複這樣做會效率低下,但這有時非常必要,而API對此為您提供了一個compact()函式。這一緩衝區工具在複製資料時要比您使用get()和put()函式高效得多。所以當您需要時,請使用compact()。
compact()之前的緩衝區:

compact()之前緩衝區檢視.png
compact()之後的緩衝區:

compact之後緩衝區檢視.png
- 這裡發生了幾件事。您會看到資料元素2-5被複制到0-3位置。位置4和5不受影響,但現在正在或已經超出了當前位置,因此是“死的”。它們可以被之後的put()呼叫重寫。
- 位置已經被設為被複制的資料元素的數目。也就是說,緩衝區現在被定位在緩衝區中最後一個“存活”元素後插入資料的位置。
- 上界屬性被設定為容量的值,因此緩衝區可以被再次填滿。
- 呼叫compact()的作用是丟棄已經釋放的資料,保留未釋放的資料,並使緩衝區對重新填充容量準備就緒。
您可以用這種 類似於先入先出(FIFO)佇列的方式使用緩衝區。當然也存在更高效的演算法(緩衝區移位並不是一個處理佇列的非常高效的方法)。但是壓縮對於使緩衝區與您從埠中讀入的資料(包)邏輯塊流的同步來說也許是一種便利的方法。
1.7、標記
標記,使緩衝區能夠記住一個位置並在之後將其返回。緩衝區的標記在mark( )函式被呼叫之前是未定義的,呼叫時標記被設為當前位置的值。reset( )函式將位置設為當前的標記值。如果標記值未定義,呼叫reset( )將導致InvalidMarkException異常。一些緩衝區函式會拋棄已經設定的標記(rewind( ),clear( ),以及flip( )總是拋棄標記)。如果新設定的值比當前的標記小,呼叫limit( )或position( )帶有索引引數的版本會拋棄標記。
1.8、比較
有時候比較兩個緩衝區所包含的資料是很有必要的。所有的緩衝區都提供了一個常規的equals( )函式用以測試兩個緩衝區的是否相等,以及一個compareTo( )函式用以比較緩衝區。
public boolean equals (Object ob)
public int compareTo (Object ob)
如果每個緩衝區中剩餘的內容相同,那麼equals( )函式將返回true,否則返回false。因為這個測試是用於嚴格的相等而且是可換向的。前面的程式清單中的緩衝區名稱可以顛倒,並會產生相同的結果。
兩個緩衝區被認為相等的充要條件是:
- 兩個物件型別相同。包含不同資料型別的buffer永遠不會相等,而且buffer絕不會等於非buffer物件。
- 兩個物件都剩餘同樣數量的元素。Buffer的容量不需要相同,而且緩衝區中剩餘資料的索引也不必相同。但每個緩衝區中剩餘元素的數目(從位置到上界)必須相同。
- 在每個緩衝區中應被Get()函式返回的剩餘資料元素序列必須一致。
如果不滿足以上任意條件,就會返回false。
相等的緩衝區:

相等的緩衝區.png
不等的緩衝區:

不等的緩衝區.png
緩衝區也支援用compareTo( )函式以詞典順序進行比較。這一函式在緩衝區引數小於,等於,或者大於引用compareTo( )的物件例項時,分別返回一個負整數,0和正整數。這些就是所有典型的緩衝區所實現的java.lang.Comparable介面語義。這意味著緩衝區陣列可以通過呼叫java.util.Arrays.sort()函式按照它們的內容進行排序。
與equals( )相似,compareTo( )不允許不同物件間進行比較。但compareTo( )更為嚴格:如果您傳遞一個型別錯誤的物件,它會丟擲ClassCastException異常,但equals( )只會返回false。
比較是針對每個緩衝區內剩餘資料進行的,與它們在equals( )中的方式相同,直到不相等的元素被發現或者到達緩衝區的上界。如果一個緩衝區在不相等元素髮現前已經被耗盡,較短的緩衝區被認為是小於較長的緩衝區。不像equals( ),compareTo( )不可交換:順序問題。
1.9、批量移動
buffer API提供了向緩衝區內外批量移動資料元素的函式:
public CharBuffer get (char [] dst) :
public CharBuffer get (char [] dst, int offset, int length)
public final CharBuffer put (char、[] src)
public CharBuffer put (char [] src, int offset, int length)
public CharBuffer put (CharBuffer src)
public final CharBuffer put (String src)
public CharBuffer put (String src, int start, int end)
有兩種形式的get( )可供從緩衝區到陣列進行的資料複製使用。第一種形式只將一個數組作為引數,將一個緩衝區釋放到給定的陣列。第二種形式使用offset和length引數來指定目標陣列的子區間。這些批量移動的合成效果與前文所討論的迴圈是相同的,但是這些方法可能高效得多,因為這種緩衝區實現能夠利用原生代碼或其他的優化來移動資料。
如果您所要求的數量的資料不能被傳送,那麼不會有資料被傳遞,緩衝區的狀態保持不變,同時丟擲BufferUnderflowException異常。因此當您傳入一個數組並且沒有指定長度,您就相當於要求整個陣列被填充。如果緩衝區中的資料不夠完全填滿陣列,您會得到一個異常。這意味著如果您想將一個小型緩衝區傳入一個大型陣列,您需要明確地指定緩衝區中剩餘的資料長度。
對於以下幾種情況的資料複製會發生異常:
- 如果你所要求的數量的資料不能被傳送,那麼不會有資料被傳遞,緩衝區的狀態保持不變,同時丟擲BufferUnderflowException異常。
- 如果緩衝區中的資料不夠完全填滿陣列,你會得到一個異常。這意味著如果你想將一個小型緩衝區傳入一個大型陣列,你需要明確地指定緩衝區中剩餘的資料長度。
2、建立緩衝區
public static CharBuffer allocate (int capacity):建立容量為capacity的緩衝區
public static CharBuffer wrap (char [] array) :將array陣列包裝成緩衝區
public static CharBuffer wrap (char [] array, int offset, int length) :將array包裝為緩衝區,position=offset,limit=lenth
public final boolean hasArray( ) :判斷緩衝區底層實現是否為陣列
public final char [] array( ) :返回緩衝中的陣列
public final int arrayOffset( ) :返回緩衝區資料在陣列中儲存的開始位置的偏移量;
新的緩衝區是由分配或包裝操作建立的。分配操作建立一個緩衝區物件並分配一個私有的空間來儲存容量大小的資料元素。包裝操作建立一個緩衝區物件但是不分配任何空間來儲存資料元素。它使用您所提供的陣列作為儲存空間來儲存緩衝區中的資料元素。
要分配一個容量為100個char變數的Charbuffer:
CharBuffer charBuffer = CharBuffer.allocate (100);
這段程式碼隱含地從堆空間中分配了一個char型陣列作為備份儲存器來儲存100個char變數。
如果您想提供您自己的陣列用做緩衝區的備份儲存器,請呼叫wrap()函式:
char [] myArray = new char [100]; CharBuffer charbuffer = CharBuffer.wrap (myArray);
這段程式碼構造了一個新的緩衝區物件,但資料元素會存在於陣列中。這意味著通過呼叫put()函式造成的對緩衝區的改動會直接影響這個陣列,而且對這個陣列的任何改動也會對這個緩衝區物件可見。帶有offset和length作為引數的wrap()函式版本則會 構造一個按照您提供的offset和length引數值初始化位置和上界的緩衝區。
通過allocate()或者wrap()函式建立的緩衝區通常都是間接的。間接的緩衝區使用備份陣列,像我們之前討論的,您可以通過上面列出的API函式獲得對這些陣列的存取權。Boolean型函式hasArray()告訴您這個緩衝區是否有一個可存取的備份陣列。如果這個函式的返回true,array()函式會返回這個緩衝區物件所使用的陣列儲存空間的引用。
如果hasArray()函式返回false,不要呼叫array()函式或者arrayOffset()函式。如果您這樣做了您會得到一個UnsupportedOperationException異常。如果一個緩衝區是隻讀的,它的備份陣列將會是超出上界的,即使一個數組物件被提供給wrap()函式。呼叫array()函式或者arrayOffset()會丟擲一個ReadOnlyBufferException異常,來阻止您得到存取權來修改只讀緩衝區的內容。如果您通過其它的方式獲得了對備份陣列的存取許可權,對這個陣列的修改也會直接影響到這個只讀緩衝區。
最後一個函式,arrayOffset(),返回緩衝區資料在陣列中儲存的開始位置的偏移量(從陣列頭0開始計算)。如果您使用了帶有三個引數的版本的wrap()函式來建立一個緩衝區,對於這個緩衝區,arrayOffset()會一直返回0,像我們之前討論的那樣。然而,如果您切分了由一個數組提供儲存的緩衝區,得到的緩衝區可能會有一個非0的陣列偏移量。這個陣列偏移量和緩衝區容量值會告訴您陣列中哪些元素是被緩衝區使用的。
3、複製緩衝區
緩衝區不限於管理陣列中的外部資料,它們也能管理其他緩衝區中的外部資料。當一個管理其他緩衝器所包含的資料元素的緩衝器被建立時,這個緩衝器被稱為檢視緩衝器。
檢視儲存器總是通過呼叫已存在的儲存器例項中的函式來建立。使用已存在的儲存器例項中的工廠方法意味著檢視物件為原始儲存器的你部實現細節私有。資料元素可以直接存取,無論它們是儲存在陣列中還是以一些其他的方式,而不需經過原始緩衝區物件的 get()/put() API。如果原始緩衝區是直接緩衝區,該緩衝區(檢視緩衝區)的檢視會具有同樣的效率優勢。
public abstract class CharBuffer extends Buffer implements CharSequence, Comparable { public abstract CharBuffer duplicate( ); public abstract CharBuffer asReadOnlyBuffer( ); public abstract CharBuffer slice( ); }
Duplicate():
Duplicate()函式建立了一個與原始緩衝區相似的新緩衝區。兩個緩衝區共享資料元素,擁有同樣的容量,但每個緩衝區擁有各自的位置,上界和標記屬性。對一個緩衝區內的資料元素所做的改變會反映在另外一個緩衝區上。這一副本緩衝區具有與原始緩衝區同樣的資料檢視。如果原始的緩衝區為只讀,或者為直接緩衝區,新的緩衝區將繼承這些屬性。
asReadOnlyBuffer():
asReadOnlyBuffer() 函式來生成一個只讀的緩衝區檢視。這與duplicate() 相同,除了這個新的緩衝區不允許使用 put(),並且其 isReadOnly() 函式將會返回true。
如果一個只讀的緩衝區與一個可寫的緩衝區共享資料,或者有包裝好的備份陣列,那麼對這個可寫的緩衝區或直接對這個陣列的改變將反映在所有關聯的緩衝區上,包括只讀緩衝區。
slice():
分割緩衝區與複製相似,但 slice() 建立一個從原始緩衝區的當前 position 開始的新緩衝區,並且其容量是原始緩衝區的剩餘元素數量(limit - position)。這個新緩衝區與原始緩衝區共享一段資料元素子序列。分割出來的緩衝區也會繼承只讀和直接屬性。slice() 分割緩衝區:
4、位元組緩衝區
4.1、位元組緩衝區
ByteBuffer 只是 Buffer 的一個子類,但位元組緩衝區有位元組的獨特之處。位元組緩衝區跟其他緩衝區型別最明顯的不同在於,它可以成為通道所執行的 I/O 的源頭或目標,後面你會發現通道只接收 ByteBuffer 作為引數。
位元組是作業系統及其 I/O 裝置使用的基本資料型別。當在 JVM 和作業系統間傳遞資料時,將其他的資料型別拆分成構成它們的位元組是十分必要的,系統層次的 I/O 面向位元組的性質可以在整個緩衝區的設計以及它們互相配合的服務中感受到。同時,作業系統是在記憶體區域中進行 I/O 操作。這些記憶體區域,就作業系統方面而言,是相連的位元組序列。於是,毫無疑問,只有位元組緩衝區有資格參與 I/O 操作。
public abstract class ByteBuffer extends Buffer implements Comparable { public static ByteBuffer allocate (int capacity) public static ByteBuffer allocateDirect (int capacity) public abstract boolean isDirect( ); public static ByteBuffer wrap (byte[] array, int offset, int length) public static ByteBuffer wrap (byte[] array) public abstract ByteBuffer duplicate( ); public abstract ByteBuffer asReadOnlyBuffer( ); public abstract ByteBuffer slice( ); public final boolean hasArray( ) public final byte [] array( ) public final int arrayOffset( ) public abstract byte get( ); public abstract byte get (int index); public ByteBuffer get (byte[] dst, int offset, int length) public ByteBuffer get (byte[] dst, int offset, int length) public abstract ByteBuffer put (byte b); public abstract ByteBuffer put (int index, byte b); public ByteBuffer put (ByteBuffer src) public ByteBuffer put (byte[] src, int offset, int length) public final ByteBuffer put (byte[] src) public final ByteOrder order( ) public final ByteBuffer order (ByteOrder bo) public abstract CharBuffer asCharBuffer( ); public abstract ShortBuffer asShortBuffer( ); public abstract IntBuffer asIntBuffer( ); public abstract LongBuffer asLongBuffer( ); public abstract FloatBuffer asFloatBuffer( ); public abstract DoubleBuffer asDoubleBuffer( ); public abstract char getChar( ); public abstract char getChar (int index); public abstract ByteBuffer putChar (char value); public abstract ByteBuffer putChar (int index, char value); public abstract short getShort( ); public abstract short getShort (int index); public abstract ByteBuffer putShort (short value); public abstract ByteBuffer putShort (int index, short value); public abstract int getInt( ); public abstract int getInt (int index); public abstract ByteBuffer putInt (int value); public abstract ByteBuffer putInt (int index, int value); public abstract long getLong( ); public abstract long getLong (int index); public abstract ByteBuffer putLong (long value); public abstract ByteBuffer putLong (int index, long value); public abstract float getFloat( ); public abstract float getFloat (int index); public abstract ByteBuffer putFloat (float value); public abstract ByteBuffer putFloat (int index, float value); public abstract double getDouble( ); public abstract double getDouble (int index); public abstract ByteBuffer putDouble (double value); public abstract ByteBuffer putDouble (int index, double value); public abstract ByteBuffer compact( ); public boolean equals (Object ob) public int compareTo (Object ob) public String toString( ) public int hashCode( ) }
多位元組數值被儲存在記憶體中的方式一般被稱為 endian-ness(位元組順序)。如果數字數值的最高位元組 - big end(大端),位於低位地址(即 big end 先寫入記憶體,先寫入的記憶體的地址是低位的,後寫入記憶體的地址是高位的),那麼系統就是大端位元組順序。如果最低位元組最先儲存在記憶體中,那麼系統就是小端位元組順序。
ByteBuffer類有所不同:預設位元組順序總是ByteBuffer.BIG_ENDIAN,無論系統的固有位元組順序是什麼。Java的預設位元組順序是大端位元組順序,這允許類檔案等以及序列化的物件可以在任何JVM中工作。如果固有硬體位元組順序是小端,這會有效能隱患。在使用固有硬體位元組順序時,將ByteBuffer的內容當作其他資料型別存取(很快就會討論到)很可能高效得多。
ByteBuffer的字元順序設定可以隨時通過呼叫以ByteOrder.BIG_ENDIAN或ByteOrder.LITTL_ENDIAN為引數的order()函式來改變。
public abstract class ByteBuffer extends Buffer implements Comparable { // This is a partial API listing public final ByteOrder order( ) public final ByteBuffer order (ByteOrder bo) }
如果一個緩衝區被建立為一個ByteBuffer物件的檢視,那麼order()返回的數值就是檢視被建立時其建立源頭的ByteBuffer的位元組順序設定。檢視的位元組順序設定在建立後不能被改變,而且如果原始的位元組緩衝區的位元組順序在之後被改變,它也不會受到影響。
4.2、直接緩衝區
核心空間(與之相對的是使用者空間,如 JVM)是作業系統所在區域,它能與裝置控制器(硬體)通訊,控制著使用者區域程序(如 JVM)的執行狀態。最重要的是,所有的 I/O 都直接(實體記憶體)或間接(虛擬記憶體)通過核心空間。
當程序(如 JVM)請求 I/O 操作的時候,它執行一個系統呼叫將控制權移交給核心。當核心以這種方式被呼叫,它隨即採取任何必要步驟,找到程序所需資料,並把資料傳送到使用者空間你的指定緩衝區。核心試圖對資料進行快取記憶體或預讀取,因此程序所需資料可能已經在核心空間裡了。如果是這樣,該資料只需簡單地拷貝出來即可。如果資料不在核心空間,則程序被掛起,核心著手把資料讀進記憶體。

緩衝區操作.png
從圖中你可能會覺得,把資料從核心空間拷貝到使用者空間似乎有些多餘。為什麼不直接讓磁碟控制器把資料送到使用者空間的緩衝區呢?首先,硬體通常不能直接訪問使用者空間。其次,像磁碟這樣基於塊儲存的硬體裝置操作的是固定大小的資料塊,而使用者程序請求的可能是任意大小的或非對齊的資料塊。在資料往來於使用者空間與儲存裝置的過程中,核心負責資料的分解、再組合工作,因此充當著中間人的角色。
因此,作業系統是在記憶體區域中進行 I/O 操作。這些記憶體區域,就作業系統方面而言,是相連的位元組序列,這也意味著I/O操作的目標記憶體區域必須是連續的位元組序列。在 JVM中,位元組陣列可能不會在記憶體中連續儲存(因為 JAVA 有 GC 機制),或者無用儲存單元(會被垃圾回收)收集可能隨時對其進行移動。
出於這個原因,引入了直接緩衝區的概念。直接位元組緩衝區通常是 I/O 操作最好的選擇。非直接位元組緩衝區(即通過 allocate() 或 wrap() 建立的緩衝區)可以被傳遞給通道,但是這樣可能導致效能損耗。通常非直接緩衝不可能成為一個本地 I/O 操作的目標。
如果你向一個通道中傳遞一個非直接 ByteBuffer 物件用於寫入,通道可能會在每次呼叫中隱含地進行下面的操作:
- 建立一個臨時的直接ByteBuffer物件。
- 將非直接緩衝區的內容複製到臨時緩衝中。
- 使用臨時緩衝區執行低層次I/O操作。
- 臨時緩衝區物件離開作用域,並最終成為被回收的無用資料。
這可能導致緩衝區在每個I/O上覆制併產生大量物件,而這種事都是我們極力避免的。不過,依靠工具,事情可以不這麼糟糕。執行時間可能會快取並重新使用直接緩衝區或者執行其他一些聰明的技巧來提高吞吐量。如果您僅僅為一次使用而建立了一個緩衝區,區別並不是很明顯。另一方面,如果您將在一段高效能指令碼中重複使用緩衝區,分配直接緩衝區並重新使用它們會使您遊刃有餘。
直接緩衝區時I/O的最佳選擇,但可能比建立非直接緩衝區要花費更高的成本。直接緩衝區使用的記憶體是通過呼叫本地作業系統方面的程式碼分配的,繞過了標準JVM堆疊。建立和銷燬直接緩衝區會明顯比具有堆疊的緩衝區更加破費,這取決於主作業系統以及JVM實現。直接緩衝區的記憶體區域不受無用儲存單元收集支配,因為它們位於標準JVM堆疊之外。
使用直接緩衝區或非直接緩衝區的效能權衡會因JVM,作業系統,以及程式碼設計而產生巨大差異。通過分配堆疊外的記憶體,您可以使您的應用程式依賴於JVM未涉及的其它力量。當加入其他的移動部分時,確定您正在達到想要的效果。我以一條舊的軟體行業格言建議您:先使其工作,再加快其執行。不要一開始就過多擔心優化問題;首先要注重正確性。JVM實現可能會執行緩衝區快取或其他的優化,5這會在不需要您參與許多不必要工作的情況下為您提供所需的效能。
直接ByteBuffer是通過呼叫具有所需容量的ByteBuffer.allocateDirect()函式產生的,就像我們之前所涉及的allocate()函式一樣。注意用一個wrap()函式所建立的被包裝的緩衝區總是非直接的。
public abstract class ByteBuffer extends Buffer implements Comparable { // This is a partial API listing public static ByteBuffer allocate (int capacity) public static ByteBuffer allocateDirect (int capacity) public abstract boolean isDirect( ); }
所有的緩衝區都提供了一個叫做isDirect()的boolean函式,來測試特定緩衝區是否為直接緩衝區。雖然ByteBuffer是唯一可以被直接分配的型別,但如果基礎緩衝區是一個直接ByteBuffer,對於非位元組檢視緩衝區,isDirect()可以是true。
4.3、檢視緩衝區
檢視緩衝區通過已存在的緩衝區物件例項的工廠方法來建立。這種檢視物件維護它自己的屬性,容量,位置,上界和標記,但是和原來的緩衝區共享資料元素。但是ByteBuffer類允許建立檢視來將byte型緩衝區位元組資料對映為其它的原始資料型別。例如,asLongBuffer()函式建立一個將八個位元組型資料當成一個long型資料來存取的檢視緩衝區。
下面列出的每一個工廠方法都在原有的ByteBuffer物件上建立一個檢視緩衝區。呼叫其中的任何一個方法都會建立對應的緩衝區型別,這個緩衝區是基礎緩衝區的一個切分,由基礎緩衝區的位置和上界決定。新的緩衝區的容量是位元組緩衝區中存在的元素數量除以檢視型別中組成一個數據型別的位元組數。在切分中任一個超過上界的元素對於這個檢視緩衝區都是不可見的。檢視緩衝區的第一個元素從建立它的ByteBuffer物件的位置開始(positon()函式的返回值)。具有能被自然數整除的資料元素個數的檢視緩衝區是一種較好的實現。
public abstract class ByteBuffer extends Buffer implements Comparable { // This is a partial API listing public abstract CharBuffer asCharBuffer( ); public abstract ShortBuffer asShortBuffer( ); public abstract IntBuffer asIntBuffer( ); public abstract LongBuffer asLongBuffer( ); public abstract FloatBuffer asFloatBuffer( ); public abstract DoubleBuffer asDoubleBuffer( ); }
一旦您得到了檢視緩衝區,您可以用duplicate(),slice()和asReadOnlyBuffer()函式建立進一步的子檢視。
無論何時一個檢視緩衝區存取一個ByteBuffer的基礎位元組,這些位元組都會根據這個檢視緩衝區的位元組順序設定被包裝成一個數據元素。當一個檢視緩衝區被建立時,檢視建立的同時它也繼承了基礎ByteBuffer物件的位元組順序設定。這個檢視的位元組排序不能再被修改。
當直接從byte型緩衝區中採集資料時,檢視換衝突擁有提高效率的潛能。如果這個檢視的位元組順序和本地機器硬體的位元組順序一致,低等級的(相對於高階語言而言)語言的程式碼可以直接存取緩衝區中的資料值,而不是通過位元資料的包裝和解包裝過程來完成。
4.4、資料元素檢視
ByteBuffer類提供了一個不太重要的機制來以多位元組資料型別的形式存取byte資料組。ByteBuffer類為每一種原始資料型別提供了存取的和轉化的方法:
public abstract class ByteBuffer extends Buffer implements Comparable { public abstract char getChar( ); public abstract char getChar (int index); public abstract short getShort( ); public abstract short getShort (int index); public abstract int getInt( ); public abstract int getInt (int index); public abstract long getLong( ); public abstract long getLong (int index); public abstract float getFloat( ); public abstract float getFloat (int index); public abstract double getDouble( ); public abstract double getDouble (int index); public abstract ByteBuffer putChar (char value); public abstract ByteBuffer putChar (int index, char value); public abstract ByteBuffer putShort (short value); public abstract ByteBuffer putShort (int index, short value); public abstract ByteBuffer putInt (int value); public abstract ByteBuffer putInt (int index, int value); public abstract ByteBuffer putLong (long value); public abstract ByteBuffer putLong (int index, long value); public abstract ByteBuffer putFloat (float value); public abstract ByteBuffer putFloat (int index, float value); public abstract ByteBuffer putDouble (double value); public abstract ByteBuffer putDouble (int index, double value); }
這些函式從當前位置開始存取ByteBuffer的位元組資料,就好像一個數據元素被儲存在那裡一樣。根據這個緩衝區的當前的有效的位元組順序,這些位元組資料會被排列或打亂成需要的原始資料型別。
這段程式碼:
int value = buffer.getInt( );
會返回一個由緩衝區中位置1-4的byte資料值組成的int型變數的值。實際的返回值取決於緩衝區的當前的位元排序(byte-order)設定。更具體的寫法是:
int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt( );
這將會返回值0x3BC5315E,同時:
int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt( );
返回值0x5E31C53B。
這些函式返回的元素不需要被任何特定模組界限所限制6。數值將會從以緩衝區的當前位置開始的位元組緩衝區中取出並組合,無論字組是否對齊。這樣的做法是低效的,但是它允許對一個位元組流中的資料進行隨機的放置。對於從二進位制檔案資料或者包裝資料成特定平臺的格式或者匯出到外部的系統,這將是非常有用的。
Put函式提供與get相反的操作。原始資料的值會根據位元組順序被分拆成一個個byte資料。如果儲存這些位元組資料的空間不夠,會丟擲BufferOverflowException。每一個函式都有過載的和無引數的形式。過載的函式對位置屬性加上特定的位元組數。然後無引數的形式則不改變位置屬性。
5、Buffer類繼承關係

Buffer類繼承關係.png
參考書籍
Ron Hitchens 《Java NIO》