緩衝區(Buffer)對象是面向塊的I/O的基礎,也是NIO的核心對象之一。在NIO中每一次I/O操做都離不開Buffer,每一次的讀和寫都是針對Buffer操做的。Buffer在實現上本質是一個數組,其做用是一個存儲器,或者分段運輸區,而且提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀/寫進程。對於傳統的流I/O,這是一種設計上的進步。html
爲了方便理解,下面我會主要採用代碼示例加註釋的方式說明緩衝區比較重要的API和知識點。java
Buffer緩衝區的家譜以下圖:api
做爲全部緩衝區類的父類,Buffer類的包含了下面4個重要屬性,
數組
// Invariants: mark <= position <= limit <= capacity private int mark = -1; private int position = 0; private int limit; private int capacity;
這4個屬性指明瞭Buffer所包含的數據元素的信息。多線程
/** * 緩衝區4屬性 * capacity 容量: 可以容納數據元素的最大數量,在緩衝區建立時指定而且不能更改。 * limit 上界: 緩衝區第一個不能被讀或寫的元素索引,也就是數據的上限位置,這個位置之後即使有數據,也是不可以訪問的。 * position 位置: 緩衝區下一個讀或寫的元素索引。 * mark 標記: 標記一個索引。調用mark()方法將會設定mark = position,調用reset()方法將設定position = mark。 * 四者之間關係始終爲 mark <= position <= limit <= capacity */ public void testNewBuffer() { CharBuffer cb = CharBuffer.allocate(10); //buffer初始設置 System.out.println(cb.capacity()); //結果爲10 System.out.println(cb.limit()); //結果爲10 System.out.println(cb.position()); //結果爲0 //mart初始值爲-1 }
下面是緩衝區主要API列表:post
public abstract class BufferAPI { public final int capacity(); //返回capacity值 public final int position(); //返回position值 public final Buffer position(int newPosition); //設置新的position值 public final int limit(); //返回limit值 public final Buffer limit(int newLimit); //設置新的limit值 public final Buffer mark(); //標記位置 mark = position public final Buffer reset(); //返回標記位置 position = mark public final Buffer clear(); //重置緩衝區的屬性到新建時的狀態,不會清楚數據 public final Buffer flip(); //緩衝區翻轉,用於讀和寫的切換 public final Buffer rewind(); //重置緩衝區position和mark屬性 public final int remaining(); //返回緩衝區可讀或寫的元素數量 public final boolean hasRemaining(); //緩衝區是否還有可讀或寫的元素 public abstract boolean isReadOnly(); //緩衝區是不是隻讀的 }
須要注意的是有些方法的返回值是Buffer,它返回的是自身的引用,這是一個精巧的類設計,容許咱們級聯的調用方法。url
/** * Buffer支持級聯用法 */ public void testCascade() { ByteBuffer bb = ByteBuffer.allocate(10); //正常調用 bb.mark(); bb.position(5); bb.reset(); //級聯調用 bb.mark().position(5).reset(); //上述2種方法是等價的,但無疑級聯調用更加美觀簡潔 }
在上面的API中並無看到存取的方法,這是由於存取的方法都定義在具體的子類中,從家譜圖看出對於除了boolean類型的其餘基本類型,緩衝區都實現了具體的子類。緩衝區本質是用數組來存放數據元素的,那麼不一樣的類型須要創建不一樣的數組。spa
緩衝區的存取是經過put()和get()方法實現的,以ByteBuffer類爲例,以下:線程
/** * buffer的存取 */ public void testPutGet() { /** * buffer的存取都經過put和get方法,而且提供了兩種方式:相對和絕對 * 相對方式:put和get的位置取決於當前的position值,調用方法後,position值會自動加1。 * 當put方法position大於緩衝區上限會拋出BufferOverflowException;一樣 * 當get方法position大於或等於緩衝區上限拋出BufferUnderflowException。 * 絕對方式:put和get須要傳入索引參數,調用方法後position值不會發生改變。當傳入的索引值 * 是負數或者大於等於緩衝區上界,拋出IndexOutOfBoundsException。 */ ByteBuffer bb = ByteBuffer.allocate(8); //相對put,position遞增 bb.put((byte)'h').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o').put((byte)'!'); bb.flip(); //翻轉緩衝區,讀寫轉換 //相對get position遞增 while(bb.hasRemaining()) { System.out.print((char)bb.get()); //輸出「hello!」 } System.out.println(); //絕對put bb.put(0, (byte)'a'); bb.put(1, (byte)'b'); bb.rewind(); //重置position,通常用於從新讀 //絕對get for(int i=0; i<bb.remaining(); i++) { System.out.print((char)bb.get(i)); ////輸出「abllo!」 } //重置緩衝區爲空狀態 以便下次使用 bb.clear(); /** * 遍歷緩衝區的兩種方法 * 1. * for(int i=0; bb.hasRemaining(); i++) * 容許多線程來訪問緩衝區,每次都會檢查緩衝區上界; * 2. * int count = bb.remaining(); * for(int i=0; i<count; i++) * 若是不存在多線程問題則會更加高效 */
/**
* 緩衝區也提供了批量存取的put和get方法
*/
}
在@Test3中,緩衝區的屬性以及數據元素的變化有必要詳細說明下,初始化的緩衝區狀態以下圖:設計
向緩衝區中填充進「hello」字節碼後,緩衝區的狀態以下:
「hello」已經都在緩衝區裏了,而後調用get方法讀取數據,以緩衝區如今的狀態執行絕對讀操做是能夠的,可是要執行相對讀就是有問題的。咱們但願的結果是把「hello」讀取出來,可是當前的position位置在「hello」以外,一直讀的結果就是讀到上界而後拋出錯誤。
解決的方法很簡單,在讀以前把position置爲0,而且把limit置爲5,這樣讀取的區間正好在「hello」範圍內。緩衝區API也封裝了這個方法flip,用於讀寫之間的轉換。flip後的緩衝區狀態以下:
這樣咱們就能夠順利的讀出緩衝區的內容。
/** * 緩衝區壓縮 * 壓縮適用於這樣的狀況:緩衝區被部分釋放後須要繼續填充, * 此時剩下的未讀數據須要向前移動到索引0的位置。 * 經過源碼能夠看到compact()方法作了3件事: * 1.將未讀數據複製到緩衝區索引0開始的位置 * 2.將position設置爲未讀數據長度的索引位置 * 3.將limit設置爲緩衝區上限 */ public void testCompact() { ByteBuffer bb = ByteBuffer.allocate(8); bb.put((byte)'h').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o').put((byte)'!'); bb.flip(); bb.get(); //釋放部分數據 bb.compact(); //壓縮 bb.put((byte)'a'); //繼續填充數據 bb.flip(); //壓縮後如需讀取,依然須要flip while(bb.hasRemaining()) { System.out.print((char)bb.get()); //遍歷結果: ello!a } }
上面的例子進行以下的圖解,當調用bb.get()後,緩衝區的狀況是這樣的:
而後調用compact()方法後,緩衝區變成下面的狀況:
/** * 比較兩個緩衝區相等 有equals和compareTo方法 * equals方法成立的條件以下: * 1. 兩個對象類型相同。包含不一樣數據類型的 buffer 永遠不會相等,並且 buffer * 毫不會等於非 buffer 對象。 * 2. 兩個對象都剩餘一樣數量的元素。Buffer 的容量不須要相同,並且緩衝區中剩 * 餘數據的索引也沒必要相同。但每一個緩衝區中剩餘元素的數目(從位置到上界)必須相 * 同。 * 3. 在每一個緩衝區中應被 Get()方法返回的剩餘數據元素序列必須一致。 * 簡單的說就是比較當前position到limit區間的數據元素 * * compareTo方法返回值-1,0,1 * 針對每一個緩衝區剩餘元素進行比較,直到不相等的元素被發現或者到達緩衝區的上界。 * 若是在一方達到上界尚未出現不相等的元素,元素個數少的緩衝區視爲小。 */ public void testEqualsAndCompare() { ByteBuffer buffer1 = ByteBuffer.allocate(8); ByteBuffer buffer2 = ByteBuffer.allocate(8); buffer1.put((byte)'h').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o'); buffer2.put((byte)'m').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o').put((byte)'w'); buffer1.position(0); buffer2.position(0); System.out.println("equals: " + buffer1.equals(buffer2)); //結果false System.out.println("compare: " + buffer1.compareTo(buffer2)); //結果-1 //兩個緩衝區設定區間比較 buffer1.position(1).limit(4); buffer2.position(1).limit(4); System.out.println("equals: " + buffer1.equals(buffer2)); //結果true System.out.println("compare: " + buffer1.compareTo(buffer2)); //結果0 buffer1.put(1, (byte)'z'); System.out.println("equals: " + buffer1.equals(buffer2)); //結果false System.out.println("compare: " + buffer1.compareTo(buffer2)); //結果1 }
緩衝區的建立和複製
緩衝區提供了幾種建立的方式:
/** * 分配操做建立一個緩衝區對象並分配一個私有的空間來儲存容量大小的數據元素。 */ public void testAllocate() { ByteBuffer bb = ByteBuffer.allocate(100); //這段代碼隱含地從堆空間中分配了一個byte型數組做爲備份存儲器來儲存100個byte變量。 }
/** * 包裝操做建立一個緩衝區對象可是不分配任何空間來儲存數據元素。 * 它使用您所提供的數組做爲存儲空間來儲存緩衝區中的數據元素 */ public void testWrap() { byte[] bytes = new byte[6]; ByteBuffer bb = ByteBuffer.wrap(bytes); /** * 對緩衝區的修改會影響數組,對數組的修改一樣會影響緩衝區的數據 */ bb.put((byte)'h').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o').put((byte)'!'); bb.flip(); for(byte b : bytes) { System.out.print((char)b); //結果hello! } System.out.println(); bytes[0] = (byte)'a'; //改變數組第0項 while (bb.hasRemaining()) { System.out.print((char)bb.get()); //結果aello! } /** * 帶參數的包裝方法 */ ByteBuffer bp = ByteBuffer.wrap(bytes, 2, 2); /** * 帶參數的包裝方法wrap(array, offset, length)並不意味着取數組的子集來做爲緩衝區, * offset和length屬性只是設置了緩衝區初始狀態;上面代碼表示建立了posion爲2,limit爲4, * 容量爲bytes.length的緩衝區 */ }
/** * 經過allocate和wrap方法建立的緩衝區都是間接緩衝區, * 間接緩衝區中使用備份數組,對於緩衝區備份數組java也提供了一些api */ public void testBufferArray() { byte[] bytes = new byte[6]; ByteBuffer bb = ByteBuffer.wrap(bytes); if(bb.hasArray()) //hasArray()方法判斷緩衝區是否有備份數組 { byte[] byteArr = bb.array(); //array()方法可以取得備份數組 System.out.println(bytes == byteArr); System.out.println(bb.arrayOffset()); //arrayOffset()方法返回緩衝區數據在數組中能夠存儲的開始位置 } /** * 可以得到緩衝區的備份數組就得到了對緩衝區進行存取的權限,當緩衝區被設爲只讀的時候, * 無疑是不容許獲得備份數組的。 */ ByteBuffer bRead = bb.asReadOnlyBuffer(); System.out.println(bRead.hasArray()); //輸出爲false }
/** * Duplicate()方法建立了一個與原始緩衝區類似的新緩衝區, * 兩個緩衝區共享數據元素,對一個緩衝區數據的修改將會反映在另外一個緩衝區上, * 但每一個緩衝區擁有本身獨立的position、limit、mark屬性, * 若是原始緩衝區是隻讀的或者直接緩衝區,複製的緩衝區將繼承這些屬性。 */ public void testDuplicate() { ByteBuffer orginal = ByteBuffer.allocate(8); orginal.position(3).limit(7).mark().position(5); ByteBuffer duplicate = orginal.duplicate(); orginal.clear(); System.out.println("orginal,position: " + orginal.position() + "; limit: " + orginal.limit() + "; mark: " + orginal.position()); //結果 orginal,position: 0; limit: 10; mark: 0 System.out.println("duplicate,position: " + duplicate.position() + "; limit: " + duplicate.limit() + "; mark: " + duplicate.reset().position()); //結果 duplicate,position: 5; limit: 8; mark: 3 //前面提到的asReadOnlyBuffer方法獲得的只讀緩衝區同duplicate相似 }
上例中原緩衝區和複製緩衝區的狀況以下圖:
/** * slice方法將對緩衝區進行分割,從原始緩衝區當前位置開始,直到上限 * 也就是position到limit的區間建立了新的緩衝區,新緩衝區和原始緩衝區共享一段數據元素, * 也會繼承只讀和直接屬性。 */ public void testSlice() { ByteBuffer orginal = ByteBuffer.allocate(10); orginal.position(3).limit(8); ByteBuffer slice = orginal.slice(); //分割了3-8的數據元素 }
上例分割後的緩衝區以下圖: