Buffer 類是 java.nio 的構造基礎。一個 Buffer 對象是固定數量的數據的容器,其做用是一個存儲器,或者分段運輸區,在這裏,數據可被存儲並在以後用於檢索。緩衝區能夠被寫滿或釋放。對於每一個非布爾原始數據類型都有一個緩衝區類,即 Buffer 的子類有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer,是沒有 BooleanBuffer 之說的。儘管緩衝區做用於它們存儲的原始數據類型,但緩衝區十分傾向於處理字節。非字節緩衝區能夠在後臺執行從字節或到字節的轉換,這取決於緩衝區是如何建立的。
◇ 緩衝區的四個屬性
全部的緩衝區都具備四個屬性來提供關於其所包含的數據元素的信息,這四個屬性儘管簡單,但其相當重要,需熟記於心: javascript
請切記,在使用 Buffer 時,咱們實際操做的就是這四個屬性的值。咱們發現,Buffer 類並無包括 get() 或 put() 函數。可是,每個Buffer 的子類都有這兩個函數,但它們所採用的參數類型,以及它們返回的數據類型,對每一個子類來講都是惟一的,因此它們不能在頂層 Buffer 類中被抽象地聲明。它們的定義必須被特定類型的子類所聽從。若不加特殊說明,咱們在下面討論的一些內容,都是以 ByteBuffer 爲例,固然,它固然有 get() 和 put() 方法了。
◇ 相對存取和絕對存取 java
Java代碼 api
來看看上面的代碼,有不帶索引參數的方法和帶索引參數的方法。不帶索引的 get 和 put,這些調用執行完後,position 的值會自動前進。固然,對於 put,若是調用屢次致使位置超出上界(注意,是 limit 而不是 capacity),則會拋出 BufferOverflowException 異常;對於 get,若是位置不小於上界(一樣是 limit 而不是 capacity),則會拋出 BufferUnderflowException 異常。這種不帶索引參數的方法,稱爲相對存取,相對存取會自動影響緩衝區的位置屬性。帶索引參數的方法,稱爲絕對存取,絕對存儲不會影響緩衝區的位置屬性,但若是你提供的索引值超出範圍(負數或不小於上界),也將拋出 IndexOutOfBoundsException 異常。
◇ 翻轉
咱們把 hello 這個串經過 put 存入一 ByteBuffer 中,以下所示:將 hello 存入 ByteBuffer 中 數組
Java代碼 緩存
此時,position = 6,limit = capacity = 1024。如今咱們要從正確的位置從 buffer 讀數據,咱們能夠把 position 置爲 0,那麼字符串的結束位置在哪呢?這裏上界該出場了。若是把上界設置成當前 position 的位置,即 6,那麼 limit 就是結束的位置。上界屬性指明瞭緩衝區有效內容的末端。人工實現翻轉: 安全
Java代碼 網絡
但這種從填充到釋放狀態的緩衝區翻轉是API設計者預先設計好的,他們爲咱們提供了一個很是便利的函數:buffer.flip()。另外,rewind() 函數與 flip() 類似,但不影響上界屬性,它只是將位置值設回 0。
◇ 釋放(Drain)
這裏的釋放,指的是緩衝區經過 put 填充數據後,而後被讀出的過程。上面講了,要讀數據,首先得翻轉。那麼怎麼讀呢?hasRemaining() 會在釋放緩衝區時告訴你是否已經達到緩衝區的上界: 多線程
Java代碼 app
很明顯,上面的代碼,每次都要判斷元素是否到達上界。咱們能夠作:改變後的釋放過程 函數
Java代碼
第二段代碼看起來很高效,但請注意,緩衝區並非多線程安全的。若是你想以多線程同時存取特定的緩衝區,你須要在存取緩衝區以前進行同步。所以,使用第二段代碼的前提是,你對緩衝區有專門的控制。
◇ buffer.clear()
clear() 函數將緩衝區重置爲空狀態。它並不改變緩衝區中的任何數據元素,而是僅僅將 limit 設爲容量的值,並把 position 設回 0。
◇ Compact(不知咋翻譯,壓縮?緊湊?)
有時候,咱們只想釋放出一部分數據,即只讀取部分數據。固然,你能夠把 postion 指向你要讀取的第一個數據的位置,將 limit 設置成最後一個元素的位置 + 1。可是,一旦緩衝區對象完成填充並釋放,它就能夠被從新使用了。因此,緩衝區一旦被讀取出來,已經沒有使用價值了。
以 Mellow 爲例,填充後爲 Mellow,但若是咱們僅僅想讀取 llow。讀取完後,緩衝區就能夠從新使用了。Me 這兩個位置對於咱們而言是沒用的。咱們能夠將 llow 複製至 0 - 3 上,Me 則被沖掉。可是 4 和 5 仍然爲 o 和 w。這個事咱們固然能夠自行經過 get 和 put 來完成,但 api 給咱們提供了一個 compact() 的函數,此函數比咱們本身使用 get 和 put 要高效的多。
Compact 以前的緩衝區
buffer.compact() 會使緩衝區的狀態圖以下圖所示:
Compact 以後的緩衝區
這裏發生了幾件事:
◇ 緩衝區的比較
有時候比較兩個緩衝區所包含的數據是頗有必要的。全部的緩衝區都提供了一個常規的 equals() 函數用以測試兩個緩衝區的是否相等,以及一個 compareTo() 函數用以比較緩衝區。
兩個緩衝區被認爲相等的充要條件是:
兩個被認爲是相等的緩衝區
兩個被認爲是不相等的緩衝區
緩衝區也支持用 compareTo() 函數以詞典順序進行比較,固然,這是全部的緩衝區實現了 java.lang.Comparable 語義化的接口。這也意味着緩衝區數組能夠經過調用 java.util.Arrays.sort() 函數按照它們的內容進行排序。
與 equals() 類似,compareTo() 不容許不一樣對象間進行比較。但 compareTo()更爲嚴格:若是你傳遞一個類型錯誤的對象,它會拋出 ClassCastException 異常,但 equals() 只會返回 false。
比較是針對每一個緩衝區你剩餘數據(從 position 到 limit)進行的,與它們在 equals() 中的方式相同,直到不相等的元素被發現或者到達緩衝區的上界。若是一個緩衝區在不相等元素髮現前已經被耗盡,較短的緩衝區被認爲是小於較長的緩衝區。這裏有個順序問題:下面小於零的結果(表達式的值爲 true)的含義是 buffer2 < buffer1。切記,這表明的並非 buffer1 < buffer2。
Java代碼
◇ 批量移動
緩衝區的設計目的就是爲了可以高效傳輸數據,一次移動一個數據元素並不高效。如你在下面的程序清單中所看到的那樣,buffer API 提供了向緩衝區你外批量移動數據元素的函數:
Java代碼
如你在上面的程序清單中所看到的那樣,buffer API 提供了向緩衝區內外批量移動數據元素的函數。以 get 爲例,它將緩衝區中的內容複製到指定的數組中,固然是從 position 開始咯。第二種形式使用 offset 和 length 參數來指定複製到目標數組的子區間。這些批量移動的合成效果與前文所討論的循環是相同的,可是這些方法可能高效得多,由於這種緩衝區實現可以利用本地代碼或其餘的優化來移動數據。
批量移動老是具備指定的長度。也就是說,你老是要求移動固定數量的數據元素。所以,get(dist) 和 get(dist, 0, dist.length) 是等價的。
對於如下幾種狀況的數據複製會發生異常:
若是緩衝區存有比數組能容納的數量更多的數據,你能夠重複利用以下代碼進行讀取:
Java代碼
put() 的批量版本工做方式類似,只不過它是將數組裏的元素寫入 buffer 中而已,這裏再也不贅述。
◇ 建立緩衝區
Buffer 的七種子類,沒有一種可以直接實例化,它們都是抽象類,可是都包含靜態工廠方法來建立相應類的新實例。這部分討論中,將以 CharBuffer 類爲例,對於其它六種主要的緩衝區類也是適用的。下面是建立一個緩衝區的關鍵函數,對全部的緩衝區類通用(要按照須要替換類名):
Java代碼
新的緩衝區是由分配(allocate)或包裝(wrap)操做建立的。分配(allocate)操做建立一個緩衝區對象並分配一個私有的空間來儲存容量大小的數據元素。包裝(wrap)操做建立一個緩衝區對象可是不分配任何空間來儲存數據元素。它使用你所提供的數組做爲存儲空間來儲存緩衝區中的數據元素。demos:
Java代碼
經過 allocate() 或者 wrap() 函數建立的緩衝區一般都是間接的。間接的緩衝區使用備份數組,你能夠經過上面列出的 api 函數得到對這些數組的存取權。
boolean 型函數 hasArray() 告訴你這個緩衝區是否有一個可存取的備份數組。若是這個函數的返回 true,array() 函數會返回這個緩衝區對象所使用的數組存儲空間的引用。若是 hasArray() 函數返回 false,不要調用 array() 函數或者 arrayOffset() 函數。若是你這樣作了你會獲得一個 UnsupportedOperationException 異常。
若是一個緩衝區是隻讀的,它的備份數組將會是超出 limit 的,即便一個數組對象被提供給 wrap() 函數。調用 array() 函數或 arrayOffset() 會拋出一個 ReadOnlyBufferException 異常以阻止你獲得存取權來修改只讀緩衝區的內容。若是你經過其它的方式得到了對備份數組的存取權限,對這個數組的修改也會直接影響到這個只讀緩衝區。
arrayOffset(),返回緩衝區數據在數組中存儲的開始位置的偏移量(從數組頭 0 開始計算)。若是你使用了帶有三個參數的版本的 wrap() 函數來建立一個緩衝區,對於這個緩衝區,arrayOffset() 會一直返回 0。不理解嗎?offset 和 length 只是指示了當前的 position 和 limit,是一個瞬間值,能夠經過 clear() 來從 0 從新存數據,因此 arrayOffset() 返回的是 0。固然,若是你切分(slice() 函數)了由一個數組提供存儲的緩衝區,獲得的緩衝區可能會有一個非 0 的數組偏移量。
◇ 複製緩衝區
緩衝區不限於管理數組中的外部數據,它們也能管理其餘緩衝區中的外部數據。當一個管理其餘緩衝器所包含的數據元素的緩衝器被建立時,這個緩衝器被稱爲視圖緩衝器。
視圖存儲器老是經過調用已存在的存儲器實例中的函數來建立。使用已存在的存儲器實例中的工廠方法意味着視圖對象爲原始存儲器的你部實現細節私有。數據元素能夠直接存取,不管它們是存儲在數組中仍是以一些其餘的方式,而不需通過原始緩衝區對象的 get()/put() API。若是原始緩衝區是直接緩衝區,該緩衝區(視圖緩衝區)的視圖會具備一樣的效率優點。
繼續以 CharBuffer 爲例,但一樣的操做可被用於任何基本的緩衝區類型。用於複製緩衝區的 api:
Java代碼
● duplidate()
複製一個緩衝區會建立一個新的 Buffer 對象,但並不複製數據。原始緩衝區和副本都會操做一樣的數據元素。
duplicate() 函數建立了一個與原始緩衝區類似的新緩衝區。兩個緩衝區共享數據元素,擁有一樣的容量,但每一個緩衝區擁有各自的 position、limit 和 mark 屬性。對一個緩衝區你的數據元素所作的改變會反映在另一個緩衝區上。這一副本緩衝區具備與原始緩衝區一樣的數據視圖。若是原始的緩衝區爲只讀,或者爲直接緩衝區,新的緩衝區將繼承這些屬性。duplicate() 複製緩衝區:
Java代碼
複製一個緩衝區
● asReadOnlyBuffer()
asReadOnlyBuffer() 函數來生成一個只讀的緩衝區視圖。這與duplicate() 相同,除了這個新的緩衝區不容許使用 put(),而且其 isReadOnly() 函數將會返回 true。
若是一個只讀的緩衝區與一個可寫的緩衝區共享數據,或者有包裝好的備份數組,那麼對這個可寫的緩衝區或直接對這個數組的改變將反映在全部關聯的緩衝區上,包括只讀緩衝區。
● slice()
分割緩衝區與複製類似,但 slice() 建立一個從原始緩衝區的當前 position 開始的新緩衝區,而且其容量是原始緩衝區的剩餘元素數量(limit - position)。這個新緩衝區與原始緩衝區共享一段數據元素子序列。分割出來的緩衝區也會繼承只讀和直接屬性。slice() 分割緩衝區:
Java代碼
建立分割緩衝區
◇ 字節緩衝區(ByteBuffer)
ByteBuffer 只是 Buffer 的一個子類,但字節緩衝區有字節的獨特之處。字節緩衝區跟其餘緩衝區類型最明顯的不一樣在於,它能夠成爲通道所執行的 I/O 的源頭或目標,後面你會發現通道只接收 ByteBuffer 做爲參數。
字節是操做系統及其 I/O 設備使用的基本數據類型。當在 JVM 和操做系統間傳遞數據時,將其餘的數據類型拆分紅構成它們的字節是十分必要的,系統層次的 I/O 面向字節的性質能夠在整個緩衝區的設計以及它們互相配合的服務中感覺到。同時,操做系統是在內存區域中進行 I/O 操做。這些內存區域,就操做系統方面而言,是相連的字節序列。因而,毫無疑問,只有字節緩衝區有資格參與 I/O 操做。
非字節類型的基本類型,除了布爾型都是由組合在一塊兒的幾個字節組成的。那麼必然要引出另一個問題:字節順序。
多字節數值被存儲在內存中的方式通常被稱爲 endian-ness(字節順序)。若是數字數值的最高字節 - big end(大端),位於低位地址(即 big end 先寫入內存,先寫入的內存的地址是低位的,後寫入內存的地址是高位的),那麼系統就是大端字節順序。若是最低字節最早保存在內存中,那麼系統就是小端字節順序。在 java.nio 中,字節順序由 ByteOrder 類封裝:
Java代碼
ByteOrder 類定義了決定從緩衝區中存儲或檢索多字節數值時使用哪一字節順序的常量。若是你須要知道 JVM 運行的硬件平臺的固有字節順序,請調用靜態類函數 nativeOrder()。
每一個緩衝區類都具備一個可以經過調用 order() 查詢的當前字節順序:
Java代碼
這個函數從 ByteOrder 返回兩個常量之一。對於除了 ByteBuffer 以外的其餘緩衝區類,字節順序是一個只讀屬性,而且可能根據緩衝區的創建方式而採用不一樣的值。除了 ByteBuffer,其餘經過 allocate() 或 wrap() 一個數組所建立的緩衝區將從 order() 返回與 ByteOrder.nativeOrder() 相同的數值。這是由於包含在緩衝區中的元素在 JVM 中將會被做爲基本數據直接存取。
ByteBuffer 類有所不一樣:默認字節順序老是 ByteBuffer.BIG_ENDIAN,不管系統的固有字節順序是什麼。Java 的默認字節順序是大端字節順序,這容許類文件等以及串行化的對象能夠在任何 JVM 中工做。若是固有硬件字節順序是小端,這會有性能隱患。在使用固有硬件字節順序時,將 ByteBuffer 的內容看成其餘數據類型存取極可能高效得多。
爲何 ByteBuffer 類須要一個字節順序?字節不就是字節嗎?ByteBuffer 對象像其餘基本數據類型同樣,具備大量便利的函數用於獲取和存放緩衝區內容。這些函數對字節進行編碼或解碼的方式取決於 ByteBuffer 當前字節順序的設定。ByteBuffer 的字節順序能夠隨時經過調用以 ByteOrder.BIG_ENDIAN 或 ByteOrder.LITTL_ENDIAN 爲參數的 order() 函數來改變:
Java代碼
若是一個緩衝區被建立爲一個 ByteBuffer 對象的視圖,,那麼 order() 返回的數值就是視圖被建立時其建立源頭的 ByteBuffer 的字節順序。視圖的字節順序設定在建立後不能被改變,並且若是原始的字節緩衝區的字節順序在以後被改變,它也不會受到影響。
◇ 直接緩衝區
內核空間(與之相對的是用戶空間,如 JVM)是操做系統所在區域,它能與設備控制器(硬件)通信,控制着用戶區域進程(如 JVM)的運行狀態。最重要的是,全部的 I/O 都直接(物理內存)或間接(虛擬內存)經過內核空間。
當進程(如 JVM)請求 I/O 操做的時候,它執行一個系統調用將控制權移交給內核。當內核以這種方式被調用,它隨即採起任何須要步驟,找到進程所需數據,並把數據傳送到用戶空間你的指定緩衝區。內核試圖對數據進行高速緩存或預讀取,所以進程所需數據可能已經在內核空間裏了。若是是這樣,該數據只需簡單地拷貝出來便可。若是數據不在內核空間,則進程被掛起,內核着手把數據讀進內存。
I/O 緩衝區操做簡圖
從圖中你可能會以爲,把數據從內核空間拷貝到用戶空間彷佛有些多餘。爲何不直接讓磁盤控制器把數據送到用戶空間的緩衝區呢?首先,硬件一般不能直接訪問用戶空間。其次,像磁盤這樣基於塊存儲的硬件設備操做的是固定大小的數據塊,而用戶進程請求的多是任意大小的或非對齊的數據塊。在數據往來於用戶空間與存儲設備的過程當中,內核負責數據的分解、再組合工做,所以充當着中間人的角色。
所以,操做系統是在內存區域中進行 I/O 操做。這些內存區域,就操做系統方面而言,是相連的字節序列,這也意味着I/O操做的目標內存區域必須是連續的字節序列。在 JVM中,字節數組可能不會在內存中連續存儲(由於 JAVA 有 GC 機制),或者無用存儲單元(會被垃圾回收)收集可能隨時對其進行移動。
出於這個緣由,引入了直接緩衝區的概念。直接字節緩衝區一般是 I/O 操做最好的選擇。非直接字節緩衝區(即經過 allocate() 或 wrap() 建立的緩衝區)能夠被傳遞給通道,可是這樣可能致使性能損耗。一般非直接緩衝不可能成爲一個本地 I/O 操做的目標。
若是你向一個通道中傳遞一個非直接 ByteBuffer 對象用於寫入,通道可能會在每次調用中隱含地進行下面的操做:
這可能致使緩衝區在每一個 I/O 上覆制併產生大量對象,而這種事都是咱們極力避免的。若是你僅僅爲一次使用而建立了一個緩衝區,區別並非很明顯。另外一方面,若是你將在一段高性能腳本中重複使用緩衝區,分配直接緩衝區並從新使用它們會使你遊刃有餘。
直接緩衝區可能比建立非直接緩衝區要花費更高的成本,它使用的內存是經過調用本地操做系統方面的代碼分配的,繞過了標準 JVM 堆棧,不受垃圾回收支配,由於它們位於標準 JVM 堆棧以外。
直接 ByteBuffer 是經過調用具備所需容量的 ByteBuffer.allocateDirect() 函數產生的。注意,wrap() 函數所建立的被包裝的緩衝區老是非直接的。與直接緩衝區相關的 api:
Java代碼
全部的緩衝區都提供了一個叫作 isDirect() 的 boolean 函數,來測試特定緩衝區是否爲直接緩衝區。可是,ByteBuffer 是惟一能夠被分配成直接緩衝區的 Buffer。儘管如此,若是基礎緩衝區是一個直接 ByteBuffer,對於非字節視圖緩衝區,isDirect() 能夠是 true。
◇ 視圖緩衝區
I/O 基本上能夠歸結成組字節數據的四處傳遞,在進行大數據量的 I/O 操做時,很又可能你會使用各類 ByteBuffer 類去讀取文件內容,接收來自網絡鏈接的數據,等等。ByteBuffer 類提供了豐富的 API 來建立視圖緩衝區。
視圖緩衝區經過已存在的緩衝區對象實例的工廠方法來建立。這種視圖對象維護它本身的屬性,容量,位置,上界和標記,可是和原來的緩衝區共享數據元素。
每個工廠方法都在原有的 ByteBuffer 對象上建立一個視圖緩衝區。調用其中的任何一個方法都會建立對應的緩衝區類型,這個緩衝區是基礎緩衝區的一個切分,由基礎緩衝區的位置和上界決定。新的緩衝區的容量是字節緩衝區中存在的元素數量除以視圖類型中組成一個數據類型的字節數,在切分中任一個超過上界的元素對於這個視圖緩衝區都是不可見的。視圖緩衝區的第一個元素從建立它的 ByteBuffer 對象的位置開始(positon() 函數的返回值)。來自 ByteBuffer 建立視圖緩衝區的工廠方法:
Java代碼
下面的代碼建立了一個 ByteBuffer 緩衝區的 CharBuffer 視圖。演示 7 個字節的 ByteBuffer 的 CharBuffer 視圖:
Java代碼
7 個 字節的 ByteBuffer 的 CharBuffer 視圖
◇ 數據元素視圖
ByteBuffer 類爲每一種原始數據類型提供了存取的和轉化的方法:
Java代碼
這些函數從當前位置開始存取 ByteBuffer 的字節數據,就好像一個數據元素被存儲在那裏同樣。根據這個緩衝區的當前的有效的字節順序,這些字節數據會被排列或打亂成須要的原始數據類型。
若是 getInt() 函數被調用,從當前的位置開始的四個字節會被包裝成一個 int 類型的變量而後做爲函數的返回值返回。實際的返回值取決於緩衝區的當前的比特排序(byte-order)設置。不一樣字節順序取得的值是不一樣的:
Java代碼
若是你試圖獲取的原始類型須要比緩衝區中存在的字節數更多的字節,會拋出 BufferUnderflowException。