NIO--BtyeBuffer(轉載)

 

Buffer類基本概念:

  一般而言,Buffer的數據結構是一個保存了原始數據的數組,在Java語言裏面封裝成爲一 個帶引用的對象。Buffer一般稱爲緩衝區,該緩衝區的優點在於它雖然是一個簡單數組,但是它封裝了很多數據常量以及單個對象的相關屬性。針對 Buffer而言主要有四個主要的屬性:
  • 容 量(Capacity ): 容量描述了這個緩衝區最 多能夠存放多少,也是Buffer的最大存儲元素量,這個值是在創建Buffer的時候指定的,而且不可以更改
  • 限 制(Limit ): 不能夠進行讀寫的緩衝區 的第一個元素,換句話說就是這個Buffer裏面的活動元素數量
  • 位 置(Position ): 下一個需要進行讀寫的元 素的索引,當Buffer緩衝區調用相對get()和set()方法的時候會自動更新Position的值
  • 標記( Mark ): 一個可記憶的 Position位置的值,當調用mark()方法的時候會執行mark = position,一旦調用reset()的時候就執行position = mark,和Position有點不一樣,除非進行設置,否則Mark值是不存在的。
  按照上邊的對應關係可以知道:
0 <= mark <= position <= limit <= capacity
  這幾個關係可以用下圖進行描述:
  [1]Buffer的基本操 作:
  Buffer管理(Accessing):
  一般情況下Buffer可以管理很多元素,但是在程序開發過程中我們 只需要關注裏面的活躍 元素 ,如上圖小於limit位置的這些元素,因爲這些元素是真正在IO讀寫過程需要的。當Buffer類調用了put()方法的時候,就在 原來的Buffer中插入了某個元素,而調用了get()方法過後就調用該位置的活躍元素,取出來進行讀取,而Buffer的get和put方法一直很神 祕,因爲它存在一個相對和絕對的概念:
  在相對版本 put 和get 中,Buffer本身不使用index 作爲參數,當相對方法調用的時候,直接使用position作爲基點,然後運算 調用結果返回,在相對操作的時候,如果position的值過大就有可能拋出異常 信息;同樣的相對版本的put方法調用的時候當調用元素超越了limit 的限制的時候也會拋出BufferOverflowException 的 異常 ,一般情況爲:position > limit 。
  在絕對版本 put和 get 中,Buffer的position卻不會收到影響,直接使用index 進行調用,如果index越界的時候直接拋出 Java裏面常見的越界異常 :java.lang.IndexOutOfBoundException 。
  針對get和put方法的兩種版本的理解可以查閱API看看方法 get的定義【這裏查 看的是Buffer類的子類ByteBuffer的API】 :
public abstract byte  get()  throws  BufferUnderflowException
public  ByteBuffer get(byte [] dst)  throws  BufferUnderflowException
public  ByteBuffer get(byte [] dst,int  offset,int  length)  throws  BufferUnderflowException,IndexOutOfBoundException
public abstract byte   get(int index)  throws  IndexOutOfBoundException
  【*:從上邊的API詳解裏面可以知道,Buffer本身支持的兩種方式的訪問是有原因 的,因爲Buffer本身的設計目的是爲了使得數據能夠更加高效地傳輸,同樣能夠在某一個時刻移動某些數據。當使用一個數組作爲參數的時候,整個 Buffer裏面的 position位置放置了一個記錄用的遊標,該遊標不斷地在上一次操作完結的基礎 上進行移動來完成Buffer本身的數據的讀取,這種情況下一般需要提 供一個length參數,使用該參數的目的就是爲了防止越界操作的發生。如果請求的數據沒有辦法進行傳輸,當讀取的時候沒有任何數據能夠讀取的時候,這個 緩衝區狀態就不能更改了,同時這個時候就會拋出BufferUnderflowException的異常 ,所以在向緩衝區請求的時候使用數組結構存儲時, 如果沒有指定length參數,系統會默認爲填充整個數組的長度,這種情況和上邊IO部分的緩衝區的設置方法類似。也就是說當編程過程需要將一個 Buffer數據拷貝到某個數組的時候(這裏可以指代字節數組),需要顯示指定拷貝的長度,否則該數組會填充到滿,而且一旦當滿足異常 條件:即limit 和position不匹配的時候,就會拋異常 。】
  [2]Buffer填充 (Filling):
  先看一段填充ByteBuffer的代碼:
buffer.put((byte )'H' ).put((byte )'e' ).put((byte )'l' ).put((byte )'l' ).put((byte )'o' );
  當這些字符傳入的時候都是以ASCII值存儲的,上述操作的存儲步驟圖如下:
  這裏需要留意一點就是填充的時候需要進行強制轉換 , 因爲Java裏面所有的字符格式都是16bit的Unicode格式 ,而上邊代碼裏面填充的時候使用的參數是字符的,如果不進行強制轉換會出現數據丟失 的情 況,ASCII碼錶示字符的時候使用的是8位數據,而Unicode方式保存字符本身和ASCII保存有很大的區別,爲了不出現亂碼保證存儲到緩衝區字符 的正確性,一定記住需要進行強制換轉,轉換成爲對應的字節方式保存。再繼續針對該Buffer進行填充操作:
buffer.put(0,(byte )'M' ).put((byte )'w' );
  第一個方法使用了絕對的方式使用index參數替換了緩衝區中的第一個字節,而第二個使用了相對版本的put方 法,所以最終形成的Buffer存儲結構圖爲:
  從上邊兩個操作可以知道,Buffer類在填充該Buffer的時候使用相對方法 和絕對方法有很大的區別,上圖可以看到原來存入的“Hello” 現在變成了“Mellow” ,這就是Buffer填充操作的一個縮略圖。
  [3]Buffer的反轉(Flipping)
  當我們在編程過程中填充了一個Buffer過後,就會對該Buffer進行消耗 (Draining) 操作,一般是將該Buffer傳入一個通道(Channel) 內 然後輸出。但是在Buffer傳入到通道中過後,通道會調用get()方法來獲取Buffer裏面數據,但是Buffer傳入的時候是按照順序傳入到通道 裏面的,如上邊的結構可以知道,本身存儲的數據可能爲“Mellow” ,但是當通道讀取Buffer裏面的內容的時候,有可能取到不正確的數據,原因 在於通道讀取Buffer裏面的數據的時候標記是從右邊開始的,這樣讀取的數據如果從position開始就會有問題 【*:當然不排除有這樣一種情況緩 衝區提供了讀取策略是雙向的,那麼這樣讀取出來的所有的字符就有可能是反向的】 。其實這點概念很容易理解,因爲Buffer讀取過後會按照 順序讀入到通道(Channel) 中,而通道獲取數據的時候會從最右邊的position位 置 開始,所以針對這樣的情況如果需要正確讀取裏面的內容就需要對Buffer進行反轉操作 ,該操作的手 動代碼如下:
buffer.limit(buffer.position()).position(0);
  但是因爲Java中Buffer類的API提供了類似的操作,只需要下邊的方法就可以了:
buffer.flip();
  經過flip操作過後,其存儲結構就會發生相對應的變化:
  【*:這個地方仔細想想,經過了Flip操作過後,從邏輯意義上講,確實 Buffer被反轉了,因爲這個時候通道類(Channel)讀取Buffer的時候會從position地方繼續讀取,不會出現讀取異常的情況。與其說 是 “反 轉” ,不如說是“重置” ,只是這裏的“重置” 不 會清空緩衝區裏面的數據,僅僅是 將緩衝區的limit屬性和position屬性進行重設 ,和真正調用reset方法的 時候還是存在一定區別的,至於這裏Flip翻譯稱爲“反轉” 我不做說明,只要讀者能夠理解上邊的步驟而且 知道這是一次Flip操作就可以了,這裏正確理解的是“重置position”  我們在編程中也經常看見 rewind()方法,該方法和flip()類似,但是該方法不會影響到limit的變化,它僅僅會將position設置爲0,所以可以直接使用 rewind方法進行 “重新讀取”  還需要說明一點是如果進行了兩次flip()操作 的話, 第二次操作會同時將 position和limit設置爲0 ,這樣的話如果再進行基於緩衝區的相對讀取過程就會BufferOverflowException 。
  [4]Buffer的“消費”(Draining):
  當一個Buffer緩衝區填滿數據過後,應用程序就會將該緩衝區送入一個通道內進行“消費” , 這種“消 費” 操作實際上使用通道來讀取Buffer裏面存儲的數據,如果需要讀取任何一個位置上的元素,則需要先flip操作才 能夠順利接受到該Buffer裏面的元素數據,也就是說在Channel通道調用get()方法之前先調用flip()方法 ,【*:這裏這種方式的調用是相 對調用過程,從參數可以知道,這裏的get()是相對方法的常用調用方式】 在通道“消費” Buffer 的過程中,有可能會使得position達到limit,不過Buffer類有一個判斷方法hasRemaining() , 該方法會告訴程序position 是否達到了limit ,因爲position一旦超越了limit過後會拋出BufferOverflowException 異 常,所以最好在迭代讀取Buffer裏面的內容的時候進行判斷,同時Buffer類還提供了一個remaining()方法返回目前的limit的值。
  *:Buffer並不是線程 安全的,如果需要多線程 操作一個Buffer,需要自己定義同步來操作 Buffer, 提供一個相關例子:
  ——[$] Fill和Drain兩個方法代碼例子——
package  org.susan.java.io;

import  java.nio.CharBuffer;

public class   BufferFillDrain {
    private static int  index = 0;
    private static   String [] strings = {
        "A random string value",
        "The product of an infinite number of monkeys",
        "Hey hey we're the Monkees",
        "Opening act for the Monkees: Jimi Hendrix",
        "'Scuse me while I kiss this fly'",
        "Help Me! Help Me!"
    };
    private static void  drainBuffer(CharBuffer buffer){
        while (buffer.hasRemaining()){
            System.out .print(buffer.get());
        }
        System.out .println();
    }
    private static boolean   fillBuffer(CharBuffer buffer){
        if ( index >= strings.length)  return false ;
            String  string = strings[index++];
        for (  int  i = 0; i < string.length(); i++ )
            buffer.put(string.charAt(i));
         return true ;
    }
    public static void   main(String args[])  throws  Exception{
        CharBuffer buffer = CharBuffer.allocate (100);
        while (fillBuffer(buffer)){
            buffer.flip();
            drainBuffer (buffer);
            buffer.clear();
        }
    }
}
  該方法的輸出如下:
A random string value
The product of an infinite number of monkeys
Hey hey we're the Monkees
Opening act for the Monkees: Jimi Hendrix
'Scuse me while I kiss this fly'
Help Me! Help Me!
  【*:這段輸出其實看不出來什麼問題 ,但是NIO 的效率明顯勝過IO,這個是可以通過一些測試 的例 子來證明的。】
  當一個Buffer進行了Fill和Drain操作過後,如果需要重新使用該Buffer,就 可以使用reset()方法,這裏reset()就是清空數據並且重置該Buffer,對比上邊的Flip()操作的“重置position”就 很容易理解Buffer的使用過程了。這裏列舉了很多方法,防止混淆摘錄Java API裏面的Buffer抽象類的所有方法列表,這是抽象類Buffer裏面的所有方法列表,上邊介紹的方法若沒有在此出現則就應該在它的子類中:
public abstract  Object array(): 返回底 層緩衝區的實現數組
public abstract int   arrayOffset(): 返回該緩衝區底層實現數 組的偏移量
public int   capacity(): 返回該緩衝區的容量
public  Buffer clear():清除 該 緩衝區
public  Buffer flip():反轉 此 緩衝區
public abstract boolean   hasArray(): 判斷該緩衝區是否有可訪問的底 層實現數組
public abstract boolean  hasRemaining(): 判斷該 緩衝區在當前位置和限 制 之間是否有元素
public abstract boolean   isDirect(): 判斷該緩衝區是否爲直接緩衝區
public abstract boolean   isReadOnly(): 判斷該緩衝區是否爲只讀緩衝區
public int   limit(): 返回該緩衝區的限制
public  Buffer limit(int  newLimit): 設置此緩衝區的限制
public   Buffer mark(): 在此緩衝區的位置設置標記
public int   position(): 返回此緩衝區的位置
public  Buffer position(int  newPosition): 設置此緩衝區的位置
public int   remaining(): 返回當前位置與限制之間的元素數
public  Buffer reset(): 將此緩 衝區的位置重置爲以前標 記的位置
public  Buffer rewind(): 重繞 此緩衝區
  [5]Buffer的壓縮(Compacting):
  很多時候,應用程序有可能只需要從緩衝區中讀取某一部分內容而不需要讀取所有,而有時候又需要 從原來的位置重新填充,爲了能夠進行這樣的操作,那些不需要讀取的數據要從緩衝區中清除掉,這樣才能使得第一個讀取到的元素的索引變成0,這種情況下就需 要使用compact() 操 作來完成,這個方法從上邊Buffer類的列表中可以知道並不包含該方法,但是每個Buffer子類實現裏面都包含了這個方法,使用該方法進行所需要的讀 取比使用get() 更 加高效,但是這種情況只在於讀取部分緩衝區內的內容。這裏分析一個簡單的例子:
  當上邊這樣的情況使用了 buffer.compact() 操作後,情況會演變成下邊這種樣子:
  【*:仔細分析上邊的內容,究竟發生了什麼事情呢?上邊這一段將可以讀取到的 “llow” 拷貝到了索引0-3的位 置,而4和5成爲了不可讀的部分,但是繼續移動position仍然可以讀取到但是它們這些元素已經“死亡” 了, 當調用put()方法的時候,它們就會被 覆蓋 掉,而且limit設置到 了容量位置,則該Buffer就可以重新被完全填充。當Buffer調用了compact方法過後將會放棄已經消費過的元素,而且使得該Buffer可以 重新填充, 這種方式類似一個先進先出的隊列(FIFO) ,可以這樣理解,compact()方法將position和limit之間的 數據複製到開始位置,從而爲後續的put()/read()讓出空間,position的值設置爲要賦值的數組的長度,limit值爲容量,這裏摘錄一段 網上講解的compact方法的使用場景: 如果有一個緩衝區需要寫數據,write()方法的非阻塞調用只會寫出其能夠發送的數據而不會阻塞等待所有的數據都發 送完成,因此write()方法不一定會將緩衝區中所有的元素都發出去,又假設現在需要調用read()方法,在緩衝區中沒有發送的數據後面讀入新數據, 處理方法就是設置 position = limit 和 limit = capacity ,當然在讀入新數據後,再次調用 write()方法前還需要將這些值還原,這樣做就會使得緩衝區最終耗盡,這就是該方法需要解決的主要問題 。 
  [6]Buffer的標記(Marking):
  標記方法mark()使得該Buffer能夠記住某個位置並且讓position在返回的時候不用返回初始索 引0而直接返回標記處進行操作 ,若mark沒有定義,調用reset()方法將會拋出InvalidMarkException 的 異常 ,需要注意的是不要混淆reset()方法和clear()方法,clear()方法單純清空該Buffer裏面的元素,而reset()方法在清空基礎 上還會重新設置一個Buffer的四個對應的屬性 ,其實Marking很好理解,提供一段代碼和對應的圖示:
buffer.position(2).mark().position(4);
當上邊的buffer調用了方法reset過後:
  如上邊所講,position最終回到了mark處而不是索引爲0的 位置
  [7]Buffer的比較 (Comparing):
  在很多場景,有必要針對兩個緩衝區進行比較操作,所有的Buffer 都提供了equals()方法用來比較兩個Buffer,而且提供了compareTo()方法進行比較。既然是兩個Buffer進行比較,它們的比較條 件爲:
  • 兩 個對象應該是 同類型 的,Buffer包含了 不同的數據類型就絕對不可能相等
  • 兩 個Buffer對象position到limit之間的元素數量(remaining返回值)相同,兩個Buffer的 容量可以不一樣 ,而且兩個Buffer的 索引位置也可以不一樣 ,但是Buffer的remaining(從 position到limit)方法返回值必須是相同的
  • 從remaining段的出示位置到結束位置裏面的每一個元素都 必 須相 同
  兩個相同Buffer圖示爲(equals()返回true ):
  兩個不相同的Buffer圖示爲(equals() 返回爲false ):
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章